graph-service.js
8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
* Graph Service — P1 虚拟引用寻址支撑函数(新文件,P1 专属)
*
* 不修改 P0 已有的 graph-db.js / graph-seed-service.js(那是并行工作线),
* 只通过 graph-db.js 导出的 getDb() 读写同一个 SQLite 连接。
*
* 详细需求与设计见 docs/todo/system-design/27-p1-virtual-reference.md。
*/
const path = require('path');
const { getDb } = require('./graph-db');
const ROOT = path.resolve(__dirname, '../..');
let indexEnsured = false;
/**
* 查询某个 system_agent id 对应的共享 agent.md 绝对路径。
*
* 只匹配 source_kind = 'template'(不匹配 'global_agent')——这是本次设计修复的一个
* 真实 bug:system/agents/ 和 system/templates/ 下有 6 个同名 id
* (agent-creator/digester/evolver/evolver-youtube-crawler/hunter/innovator)
* 内容可能不同步,hire() 只从 system/templates/ 拷贝过文件,如果查表命中了
* global_agent 那一行会读错文件(详见 27-graph-agent-architecture.md §7、
* 27-p1-virtual-reference.md §4.6)。
*
* @param {string} systemAgentId
* @returns {string|null} 共享 agent.md 的绝对路径;查不到对应 template 行时返回 null
*/
function lookupSharedAgentMd(systemAgentId) {
const db = getDb();
const row = db
.prepare(`SELECT legacy_path FROM system_agents WHERE id = ? AND source_kind = 'template'`)
.get(systemAgentId);
if (!row) return null;
return path.resolve(ROOT, row.legacy_path, 'agent.md');
}
/**
* P2(27-p2-org-graph.md §3.2)起改为**部分**唯一索引:只约束 status='active' 的行,
* 这样某个角色被 fire(status='removed')之后,同一个 role_name 能被重新 hire 进来。
* 索引本身 graph-db.js 的 migrate() 已经建过了,这里保留是为了让本模块不依赖调用顺序;
* 名字/条件必须与 graph-db.js 完全一致,否则会互相把对方的索引建回来。
*/
function ensureOrgGraphNodeIndex(db) {
if (indexEnsured) return;
db.exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_org_graph_nodes_company_role_active
ON org_graph_nodes(company_id, role_name) WHERE status = 'active'
`);
indexEnsured = true;
}
/**
* 保证 system_agents 里有 systemAgentId 这一行(缺则按 seedCompanyAgents()
* (graph-seed-service.js 约 213-295 行)对同样场景的字段约定 fallback 插入
* source_kind='company_hired' 的记录)。已存在的行一个字段都不改——绝不把
* source_kind='template' / 'global_agent' 的公共 agent 降级成公司私有。
*
* P3B/W7 起从 upsertOrgGraphNode() 里拆出来单独导出:work 层本体被雇时**不写**
* org_graph_nodes(判据见 company-service.decideOrgNodeRecording),但 system_agents
* 这一行仍然必须补——`work_graph_nodes.system_agent_id` 是指向该表的外键,
* 缺行的话 workgraph-planning 雇完人再提交图会直接外键挂(27-P3B §四 W1)。
*
* 幂等,可重复调用。调用方自己起事务时也安全(better-sqlite3 支持嵌套 transaction)。
*
* @param {string} companyId
* @param {string} systemAgentId
* @param {string} roleName - 仅用于 fallback 行的 legacy_path
* @returns {boolean} true = 本次真的插了一行;false = 早就有了
*/
function ensureSystemAgentRow(companyId, systemAgentId, roleName) {
const db = getDb();
const existingAgent = db.prepare(`SELECT id FROM system_agents WHERE id = ?`).get(systemAgentId);
if (existingAgent) return false;
const now = new Date().toISOString();
db.prepare(`
INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at)
VALUES (@id, @name, 'company', @owner_company_id, 'company_hired', @legacy_path, 'active', NULL, @created_at, @updated_at)
`).run({
id: systemAgentId,
name: systemAgentId,
owner_company_id: companyId,
legacy_path: `data/companies/${companyId}/agents/${roleName}`,
created_at: now,
updated_at: now,
});
return true;
}
/**
* 这家公司的 org graph 有没有成型,以及某个角色在不在图上(P3B §四 W7 的判据之一)。
*
* `hasWorkflow` 判的是「orggraph-planning 提交过一张图没有」,而**不是**「有没有节点」——
* 节点 hire() 自己就会造(P1/P2 语义),拿节点数当信号会把「同一家公司连雇两个人,
* 第二个不入图」这条既有语义废掉。
*
* 主判据是 `authored_by='planner'` 的节点(writeGraph 是唯一写它的地方)。
* 早先用的是**边数**代理,而真跑里 orggraph-planning 裁出过一张**零条边的单节点图**,
* 代理当场失效:三条判据全落空 → work 层本体被写进 org_graph_nodes,
* 下一次下达任务时被 advanceOne 当 org 节点执行(P3B W7-fix)。
* 边数保留为存量行(authored_by IS NULL)的回落判据。
*
* 此后这张图的作者就是 orggraph-planning,
* hire() 不再往上追加节点:`work-graph-db.js:473` 起 org run 时读的正是 org_graph_nodes
* 的 active 行,混进去的 work 层本体不是画歪一张图,而是真的多出一个会被执行的 org 节点。
*
* @param {string} companyId
* @param {string} roleName
* @returns {{hasWorkflow: boolean, isNode: boolean}}
*/
function orgGraphMembership(companyId, roleName) {
const db = getDb();
// 主判据:有没有 planner 写下的节点。这是「orggraph-planning 提交过一张图」的**直接**证据。
const planned = db.prepare(
`SELECT COUNT(*) AS c FROM org_graph_nodes
WHERE company_id = ? AND status = 'active' AND authored_by = 'planner'`
).get(companyId);
// 回落判据:本列之前的存量行 authored_by 是 NULL,对它们仍用原来的边数代理,行为不变。
const edges = db.prepare(
`SELECT COUNT(*) AS c FROM org_graph_edges WHERE company_id = ? AND status = 'active'`
).get(companyId);
const node = db.prepare(
`SELECT COUNT(*) AS c FROM org_graph_nodes WHERE company_id = ? AND role_name = ? AND status = 'active'`
).get(companyId, roleName);
const hasWorkflow = ((planned && planned.c) || 0) > 0 || ((edges && edges.c) || 0) > 0;
return { hasWorkflow, isNode: ((node && node.c) || 0) > 0 };
}
/**
* 为 (companyId, roleName) 建立/复用一条 org_graph_nodes 引用记录。
*
* 语义是「把这个 agent 记成本公司 org graph 的一个节点」——**显式动作**,
* 调用方有两类:org-graph-service.insertNode(规划 agent 提交 org graph)与
* company-service.hire 判定为 org 层招聘时。work 层招聘不走这里(P3B §四 W7)。
*
* system_agents 缺行时先补(ensureSystemAgentRow),因为 org_graph_nodes.system_agent_id
* 也是外键。
*
* 幂等:org_graph_nodes(company_id, role_name) WHERE status='active' 部分唯一索引
* + INSERT ... ON CONFLICT(...) WHERE status='active' DO NOTHING,重复调用不产生重复行。
* (SQLite 的 conflict target 要匹配部分索引,必须原样带上索引的 WHERE 条件。)
*
* @param {string} companyId
* @param {string} systemAgentId
* @param {string} roleName
* @returns {void}
*/
function upsertOrgGraphNode(companyId, systemAgentId, roleName, authoredBy = 'hire') {
const db = getDb();
ensureOrgGraphNodeIndex(db);
const now = new Date().toISOString();
const tx = db.transaction(() => {
ensureSystemAgentRow(companyId, systemAgentId, roleName);
db.prepare(`
INSERT INTO org_graph_nodes (company_id, system_agent_id, role_name, status, created_at, authored_by)
VALUES (@company_id, @system_agent_id, @role_name, 'active', @created_at, @authored_by)
ON CONFLICT(company_id, role_name) WHERE status = 'active' DO NOTHING
`).run({
company_id: companyId,
system_agent_id: systemAgentId,
role_name: roleName,
created_at: now,
authored_by: authoredBy,
});
// 冲突被 DO NOTHING 吞掉时行还在,但来源可能需要升格:
// hire 先记了一行(authored_by='hire'),随后 orggraph-planning 提交整张图把它纳入,
// 这行就该算 planner 写的。反向**不升格** —— planner 写过的图不会因为后续 hire 降级。
if (authoredBy === 'planner') {
db.prepare(`
UPDATE org_graph_nodes SET authored_by = 'planner'
WHERE company_id = ? AND role_name = ? AND status = 'active'
`).run(companyId, roleName);
}
});
tx();
}
module.exports = {
lookupSharedAgentMd,
ensureSystemAgentRow,
orgGraphMembership,
upsertOrgGraphNode,
};