graph-service.js
6.99 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
/**
* 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` 看的是**边**不是节点:节点 hire() 自己就会造(P1/P2 语义),拿节点当
* 「图已存在」的信号会把「同一家公司连雇两个人,第二个不入图」这条既有语义废掉。
* 有 active 边则说明 orggraph-planning 提交过一张连起来的图(POST /org-graph →
* org-graph-service.writeGraph 是唯一写边的地方),此后这张图的作者就是它,
* 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();
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);
return { hasWorkflow: ((edges && edges.c) || 0) > 0, 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) {
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)
VALUES (@company_id, @system_agent_id, @role_name, 'active', @created_at)
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,
});
});
tx();
}
module.exports = {
lookupSharedAgentMd,
ensureSystemAgentRow,
orgGraphMembership,
upsertOrgGraphNode,
};