dsh-hams 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +11335 -0
  3. package/package.json +9 -5
  4. package/src/domain/check.ts +0 -193
  5. package/src/domain/errors.ts +0 -59
  6. package/src/domain/evidence.ts +0 -134
  7. package/src/domain/ids.ts +0 -31
  8. package/src/host/chat-runtime.ts +0 -489
  9. package/src/host/dsh-adapter.ts +0 -867
  10. package/src/host/session-adapter.ts +0 -276
  11. package/src/http/handler.ts +0 -1132
  12. package/src/index.js +0 -53
  13. package/src/service/audit.ts +0 -92
  14. package/src/service/chat.ts +0 -1100
  15. package/src/service/contracts.ts +0 -193
  16. package/src/service/defaults.ts +0 -182
  17. package/src/service/deliverables.ts +0 -240
  18. package/src/service/hams.ts +0 -3103
  19. package/src/service/knowledge.ts +0 -940
  20. package/src/service/member-config.ts +0 -39
  21. package/src/service/members.ts +0 -506
  22. package/src/service/memory.ts +0 -375
  23. package/src/service/permissions.ts +0 -118
  24. package/src/service/project-lifecycle.ts +0 -233
  25. package/src/service/projects.ts +0 -840
  26. package/src/service/receipts.ts +0 -63
  27. package/src/service/templates.ts +0 -140
  28. package/src/service/tick.ts +0 -751
  29. package/src/service/tools.ts +0 -711
  30. package/src/service/types.ts +0 -219
  31. package/src/service/work-item-create.ts +0 -159
  32. package/src/service/workspace-profile.ts +0 -443
  33. package/src/store/backup.ts +0 -239
  34. package/src/store/bootstrap.ts +0 -225
  35. package/src/store/db.ts +0 -179
  36. package/src/store/lock.ts +0 -194
  37. package/src/store/migrations.ts +0 -211
  38. package/src/store/schema/001_p1.sql +0 -846
  39. package/src/store/schema/001_p1.sql.ts +0 -865
  40. package/src/store/schema/002_knowledge.sql +0 -54
  41. package/src/store/schema/002_knowledge.sql.ts +0 -68
  42. package/src/store/schema/003_projects.sql.ts +0 -3
  43. package/src/store/schema/004_team.sql.ts +0 -50
  44. package/src/store/schema/005_target_check.sql.ts +0 -22
  45. package/src/store/schema/006_target_flow.sql.ts +0 -19
  46. package/src/store/schema/007_project_delete.sql.ts +0 -17
  47. package/src/store/schema/008_memory.sql.ts +0 -47
  48. package/src/store/schema/generate-projects.mjs +0 -16
  49. package/src/store/seed.ts +0 -114
@@ -1,711 +0,0 @@
1
- /**
2
- * 模型工具层:bot 唯一的动手通道,比 HTTP 更窄。
3
- *
4
- * 三条硬规则(backend-design §10 / 兼容层 §5):
5
- * 1. **没有审批工具、没有提议决定工具、没有直接关单工具、没有任意 SQL**;
6
- * 2. 主体只从宿主执行上下文里的 `sessionId` 反查 `work_item_thread` 得到,
7
- * 参数里出现 `memberId` / `principalId` 一律拒绝(C5 的结论:`exec.agent.session.id`
8
- * 是唯一可信来源,模型无法伪造);
9
- * 3. 失败不抛错,返回 `{ok:false, error:{code,message,nextStep}}`——让模型知道下一步,
10
- * 而不是死循环盲试。
11
- *
12
- * 工具定义与 `ctx.tools.register` 的参数形状对齐(name / description / parameters),
13
- * 但这里不依赖宿主,便于单测;注册动作留给插件入口。
14
- */
15
- import type { DatabaseSync } from 'node:sqlite'
16
- import { HamsError } from '../domain/errors.ts'
17
- import type { HamsService } from './hams.ts'
18
- import type { CommandContext } from './types.ts'
19
- import {
20
- asNumber,
21
- asObject,
22
- asString,
23
- nextStepFor,
24
- optionalString,
25
- type ReceiptError,
26
- } from './contracts.ts'
27
-
28
- export interface ToolExecContext {
29
- /** 宿主注入的服务端会话 id;模型无法伪造。 */
30
- sessionId: string
31
- callId?: string
32
- }
33
-
34
- export interface ToolResult {
35
- ok: boolean
36
- data?: unknown
37
- error?: ReceiptError
38
- }
39
-
40
- export interface ToolDefinition {
41
- name: string
42
- description: string
43
- parameters: Record<string, unknown>
44
- execute(args: unknown, exec: ToolExecContext): ToolResult | Promise<ToolResult>
45
- }
46
-
47
- /** 项目群能力(由 ChatService 实现);宿主没接会话运行时就报 HOST_CAPABILITY_MISSING。 */
48
- export interface GroupChatPort {
49
- projectOfSession(sessionId: string): string | null
50
- groupMembersBySession(sessionId: string): Array<{ memberId: string; name: string; kind: string; status: string; brief: string }>
51
- groupSpeakBySession(sessionId: string, input: { toMemberId: string; text: string }): Promise<{ ok: true; messageId: string }>
52
- groupSpawnBySession(sessionId: string, input: { name: string; brief?: string }): { memberId: string; name: string }
53
- /** 指派后在群里留下可见的 @ 记录(可选;没有实现的宿主忽略)。 */
54
- noteAssignment?(sessionId: string, input: { memberId: string; text: string }): { ok: true; messageId: string } | null
55
- /** 把值得记住的内容写进项目记忆;能力类同步给对应 bot。 */
56
- rememberBySession?(
57
- sessionId: string,
58
- input: { title: string; body: string; kind: string; botId?: string | null },
59
- ): { ok: true; memoryId: string; created: boolean; syncedTo: string | null }
60
- }
61
-
62
- interface ThreadIdentity {
63
- workItemId: string
64
- memberId: string
65
- threadId: string
66
- generation: number
67
- status: string
68
- }
69
-
70
- export interface HamsToolsOptions {
71
- service: HamsService
72
- db: DatabaseSync
73
- /** 覆盖默认的线程解析(测试或未来的多宿主适配)。 */
74
- resolveThread?: (db: DatabaseSync, sessionId: string) => ThreadIdentity | null
75
- /**
76
- * 首次认领时还没有线程:由宿主适配器把「它自己创建的会话」映射到 bot 成员。
77
- * 这只是会话创建与首个 turn 之间的桥;M5 会在开会话时先建线程,届时可以不用它。
78
- */
79
- resolveSessionOwner?: (sessionId: string) => { memberId: string } | null
80
- idempotencyFor?: (exec: ToolExecContext, tool: string) => string | undefined
81
- /** 项目群:成员名单、@ 发言、建临时 bot。没注入时这些工具明确报不可用。 */
82
- chat?: GroupChatPort
83
- }
84
-
85
- export function resolveThreadIdentity(db: DatabaseSync, sessionId: string): ThreadIdentity | null {
86
- const row = db
87
- .prepare(
88
- `SELECT id, work_item_id, member_id, generation, status
89
- FROM work_item_thread WHERE session_id = ?
90
- ORDER BY generation DESC LIMIT 1`,
91
- )
92
- .get(sessionId) as
93
- | { id: string; work_item_id: string; member_id: string; generation: number; status: string }
94
- | undefined
95
- if (!row) return null
96
- return {
97
- threadId: row.id,
98
- workItemId: row.work_item_id,
99
- memberId: row.member_id,
100
- generation: row.generation,
101
- status: row.status,
102
- }
103
- }
104
-
105
- function fail(error: unknown): ToolResult {
106
- if (error instanceof HamsError) {
107
- return {
108
- ok: false,
109
- error: {
110
- code: error.code,
111
- message: error.message,
112
- nextStep: error.nextStep ?? nextStepFor(error.code),
113
- detail: error.detail,
114
- },
115
- }
116
- }
117
- return {
118
- ok: false,
119
- error: {
120
- code: 'STATE_INVALID',
121
- message: error instanceof Error ? error.message : String(error),
122
- nextStep: nextStepFor('STATE_INVALID'),
123
- },
124
- }
125
- }
126
-
127
- /** 参数里禁止出现身份字段;出现即拒绝,不给「试试看」的空间。 */
128
- function assertNoIdentityArgs(args: Record<string, unknown>): void {
129
- for (const field of ['memberId', 'member_id', 'principalId', 'principal_id', 'actorId', 'actor_id']) {
130
- if (field in args) {
131
- throw new HamsError('POLICY_DENIED', '身份由服务端从会话解析,参数里不接受任何身份字段')
132
- }
133
- }
134
- }
135
-
136
- export function createHamsTools(options: HamsToolsOptions): ToolDefinition[] {
137
- const { service, db } = options
138
- const resolve = options.resolveThread ?? resolveThreadIdentity
139
- let counter = 0
140
-
141
- /** 需要绑定工作项的会话(证据、提交结果、复查、阻塞、请示关单):对话会话不允许做这些。 */
142
- function identity(exec: ToolExecContext): ThreadIdentity {
143
- const source = memberIdentity(exec)
144
- if (!source.thread) {
145
- throw new HamsError('POLICY_DENIED', `这个操作要在工作项会话里做,当前是对话会话:${exec.sessionId}`, {
146
- nextStep: '把这个动作交给工作项里的运行;对话里只能查待办、看项目工作项、指派与提议',
147
- })
148
- }
149
- return source.thread
150
- }
151
-
152
- /** 认人会话:工作项线程 → 宿主映射 → 对话会话反向索引;拿不到才拒绝。 */
153
- function memberIdentity(exec: ToolExecContext): { memberId: string; thread: ThreadIdentity | null } {
154
- if (!exec?.sessionId || !exec.sessionId.trim()) {
155
- throw new HamsError('HOST_CAPABILITY_MISSING', '拿不到服务端会话 id,无法确定调用者身份')
156
- }
157
- const thread = resolve(db, exec.sessionId)
158
- if (thread) return { memberId: thread.memberId, thread }
159
- const owner = options.resolveSessionOwner?.(exec.sessionId)
160
- if (owner?.memberId) return { memberId: owner.memberId, thread: null }
161
- // 对话会话(单聊/项目群聊)也属于某个 bot:查 HAMS 的反向索引,而不是让模型自报身份。
162
- const chat = db.prepare('SELECT value FROM hams_meta WHERE key = ?').get(`chat:session:${exec.sessionId}`) as
163
- | { value: string }
164
- | undefined
165
- if (chat && chat.value) return { memberId: chat.value, thread: null }
166
- throw new HamsError('POLICY_DENIED', `会话未绑定任何工作项,拒绝执行:${exec.sessionId}`)
167
- }
168
-
169
- function context(
170
- identityValue: { memberId: string } | ThreadIdentity,
171
- exec: ToolExecContext,
172
- tool: string,
173
- ): CommandContext {
174
- counter += 1
175
- return {
176
- principalId: identityValue.memberId,
177
- channel: 'tool',
178
- correlationId: exec.callId ?? `tool-${tool}-${counter}`,
179
- idempotencyKey: options.idempotencyFor?.(exec, tool) ?? (exec.callId ? `${tool}:${exec.callId}` : undefined),
180
- }
181
- }
182
-
183
- function run<T>(fn: () => T): ToolResult {
184
- try {
185
- return { ok: true, data: fn() }
186
- } catch (error) {
187
- return fail(error)
188
- }
189
- }
190
-
191
- async function runAsync(fn: () => Promise<unknown>): Promise<ToolResult> {
192
- try {
193
- return { ok: true, data: await fn() }
194
- } catch (error) {
195
- return fail(error)
196
- }
197
- }
198
-
199
- function groupPort(): GroupChatPort {
200
- if (!options.chat) {
201
- throw new HamsError('HOST_CAPABILITY_MISSING', '当前实例没有接宿主会话服务,群聊工具不可用', {
202
- nextStep: '重启 DSH 让插件接上 agents 服务',
203
- })
204
- }
205
- return options.chat
206
- }
207
-
208
- function ownedItemCheck(thread: ThreadIdentity, workItemId: string): void {
209
- if (thread.workItemId !== workItemId) {
210
- throw new HamsError('POLICY_DENIED', '本会话只被绑定到一个工作项,不能操作其它工作项')
211
- }
212
- }
213
-
214
- const tools: ToolDefinition[] = [
215
- {
216
- name: 'hams_list_my_work',
217
- description: '列出指派给我的或可认领的工作项。只读。',
218
- parameters: {
219
- type: 'object',
220
- properties: { status: { type: 'string', description: '按状态过滤' } },
221
- additionalProperties: false,
222
- },
223
- execute(rawArgs, exec) {
224
- return run(() => {
225
- const member = memberIdentity(exec)
226
- const args = rawArgs === undefined ? {} : asObject(rawArgs, 'args')
227
- assertNoIdentityArgs(args)
228
- const status = optionalString(args.status, 'status')
229
- const items = service.listWorkItems(context(member, exec, 'list'), { limit: 50 })
230
- return items.filter((item) => {
231
- const mine = item.assignee_id === member.memberId
232
- const claimable = item.status === 'ready'
233
- if (!mine && !claimable) return false
234
- return status ? item.status === status : true
235
- })
236
- })
237
- },
238
- },
239
- {
240
- name: 'hams_get_work_item',
241
- description: '读取工作项详情与当前允许的动作(allowedActions)。只读。',
242
- parameters: {
243
- type: 'object',
244
- properties: { id: { type: 'string' } },
245
- required: ['id'],
246
- additionalProperties: false,
247
- },
248
- execute(rawArgs, exec) {
249
- return run(() => {
250
- const member = memberIdentity(exec)
251
- const args = asObject(rawArgs, 'args')
252
- assertNoIdentityArgs(args)
253
- const id = asString(args.id, 'id')
254
- return service.getWorkItem(context(member, exec, 'get'), id)
255
- })
256
- },
257
- },
258
- {
259
- name: 'hams_claim',
260
- description: '认领一个 ready 工作项,建立租约与运行。',
261
- parameters: {
262
- type: 'object',
263
- properties: {
264
- id: { type: 'string' },
265
- expectedEpoch: { type: 'number', description: '调用方看到的执行代次,用于拒绝过期操作' },
266
- },
267
- required: ['id'],
268
- additionalProperties: false,
269
- },
270
- execute(rawArgs, exec) {
271
- return run(() => {
272
- const identityValue = memberIdentity(exec)
273
- const args = asObject(rawArgs, 'args')
274
- assertNoIdentityArgs(args)
275
- const id = asString(args.id, 'id')
276
- const expectedEpoch = args.expectedEpoch === undefined ? undefined : asNumber(args.expectedEpoch, 'expectedEpoch')
277
- if (expectedEpoch !== undefined) {
278
- const item = service.getWorkItem(context(identityValue, exec, 'get'), id)
279
- if (item.execution_epoch !== expectedEpoch) {
280
- throw new HamsError('EPOCH_STALE', `执行代次已变:期望 ${expectedEpoch},当前 ${item.execution_epoch}`)
281
- }
282
- }
283
- return service.claim(context(identityValue, exec, 'claim'), {
284
- workItemId: id,
285
- sessionId: exec.sessionId,
286
- })
287
- })
288
- },
289
- },
290
- {
291
- name: 'hams_assign_task',
292
- description: '把一个待处理的工作项指派给另一个在岗 bot。只有项目的推进 bot(coordinator)或有人工管理权限的人可以指派。指派后必须在群聊里用 hams_group_send 写出「@该 bot 名字」说明任务,让人类看得见派给了谁。',
293
- parameters: {
294
- type: 'object',
295
- properties: {
296
- id: { type: 'string', description: '工作项 id' },
297
- assigneeId: { type: 'string', description: '接任务的 bot 成员 id' },
298
- },
299
- required: ['id', 'assigneeId'],
300
- additionalProperties: false,
301
- },
302
- execute(rawArgs, exec) {
303
- return run(() => {
304
- const identity = memberIdentity(exec)
305
- const args = asObject(rawArgs, 'args')
306
- assertNoIdentityArgs(args)
307
- const workItemId = asString(args.id, 'id')
308
- const assigneeId = asString(args.assigneeId, 'assigneeId')
309
- const result = service.assignWorkItem(context(identity, exec, 'assign'), { workItemId, memberId: assigneeId })
310
- // 指派同时,在群聊里打出 @接活人:人类要看得见「派给了谁」。
311
- try {
312
- groupPort().noteAssignment?.(exec.sessionId, {
313
- memberId: assigneeId,
314
- text: `请接手工作项 ${workItemId},在群里说明下一步并接单。`,
315
- })
316
- } catch {
317
- // 不在项目群会话里(或宿主没接群聊):指派本身已经成功,不因此失败。
318
- }
319
- return result
320
- })
321
- },
322
- },
323
- {
324
- name: 'hams_list_project_work',
325
- description: '列出一个项目下的工作项(状态、负责人、下一步、截止),用于在项目群聊里回答“进展到哪了”。',
326
- parameters: {
327
- type: 'object',
328
- properties: {
329
- projectId: { type: 'string' },
330
- status: { type: 'string', description: '可选:只看某个状态' },
331
- },
332
- required: ['projectId'],
333
- additionalProperties: false,
334
- },
335
- execute(rawArgs, exec) {
336
- return run(() => {
337
- const identity = memberIdentity(exec)
338
- const args = asObject(rawArgs, 'args')
339
- assertNoIdentityArgs(args)
340
- return service
341
- .listWorkItems(context(identity, exec, 'list-project'), {
342
- projectId: asString(args.projectId, 'projectId'),
343
- status: optionalString(args.status, 'status'),
344
- limit: 100,
345
- })
346
- .map((row) => ({
347
- id: row.id,
348
- title: row.title,
349
- status: row.status,
350
- assigneeId: row.assignee_id,
351
- nextActionAt: row.next_action_at,
352
- dueAt: row.due_at,
353
- updatedAt: row.updated_at,
354
- }))
355
- })
356
- },
357
- },
358
- {
359
- name: 'hams_group_members',
360
- description: '列出现在这个项目群里的成员(推进 bot、手动加入的 bot、临时 bot)。只读。',
361
- parameters: { type: 'object', properties: {}, additionalProperties: false },
362
- execute(rawArgs, exec) {
363
- return run(() => {
364
- memberIdentity(exec)
365
- assertNoIdentityArgs(rawArgs === undefined ? {} : asObject(rawArgs, 'args'))
366
- return groupPort().groupMembersBySession(exec.sessionId)
367
- })
368
- },
369
- },
370
- {
371
- name: 'hams_group_send',
372
- description: '在项目群里 @ 某个成员说一句话;正文会自动以「@成员名」开头,对方的回答会合并进群聊记录。派活必须用它,不能只在心里指派。',
373
- parameters: {
374
- type: 'object',
375
- properties: {
376
- toMemberId: { type: 'string', description: '要 @ 的群成员 id(先用 hams_group_members 查)' },
377
- text: { type: 'string' },
378
- },
379
- required: ['toMemberId', 'text'],
380
- additionalProperties: false,
381
- },
382
- execute(rawArgs, exec) {
383
- return runAsync(async () => {
384
- memberIdentity(exec)
385
- const args = asObject(rawArgs, 'args')
386
- assertNoIdentityArgs(args)
387
- return groupPort().groupSpeakBySession(exec.sessionId, {
388
- toMemberId: asString(args.toMemberId, 'toMemberId'),
389
- text: asString(args.text, 'text'),
390
- })
391
- })
392
- },
393
- },
394
- {
395
- name: 'hams_remember',
396
- description:
397
- '在项目群里,把推进过程中值得记住的内容记进项目记忆(事实/决策/偏好/教训/能力)。遇到决定、口径、教训、人的偏好或 bot 能力经验时使用;kind=capability 时会同步一份给对应 bot 的记忆,botId 省略则默认为当前 bot。',
398
- parameters: {
399
- type: 'object',
400
- properties: {
401
- title: { type: 'string', description: '一句话标题(最多 120 字)' },
402
- body: { type: 'string', description: '记忆正文:保留了哪些事实、为什么、以后怎么做' },
403
- kind: {
404
- type: 'string',
405
- enum: ['fact', 'decision', 'preference', 'lesson', 'capability'],
406
- description: '事实 / 决策 / 偏好 / 教训 / 能力',
407
- },
408
- botId: { type: 'string', description: '能力类归属的 bot 成员 id;省略 = 当前 bot' },
409
- },
410
- required: ['title', 'body', 'kind'],
411
- additionalProperties: false,
412
- },
413
- execute(rawArgs, exec) {
414
- return run(() => {
415
- memberIdentity(exec)
416
- const args = asObject(rawArgs, 'args')
417
- assertNoIdentityArgs(args)
418
- const port = groupPort()
419
- if (!port.rememberBySession) throw new HamsError('HOST_CAPABILITY_MISSING', '当前宿主没有接项目记忆服务')
420
- return port.rememberBySession(exec.sessionId, {
421
- title: asString(args.title, 'title'),
422
- body: asString(args.body, 'body'),
423
- kind: asString(args.kind, 'kind'),
424
- botId: optionalString(args.botId, 'botId'),
425
- })
426
- })
427
- },
428
- },
429
- {
430
- name: 'hams_group_spawn_bot',
431
- description: '在项目群里创建一个只属于这个项目的临时 bot(之后可用 hams_group_send @ 它,正文要写出 @名字)。只有推进 bot 能建。',
432
- parameters: {
433
- type: 'object',
434
- properties: {
435
- name: { type: 'string', description: '临时 bot 的名字(最多 24 字)' },
436
- brief: { type: 'string', description: '给它的一句话职责说明' },
437
- },
438
- required: ['name'],
439
- additionalProperties: false,
440
- },
441
- execute(rawArgs, exec) {
442
- return run(() => {
443
- memberIdentity(exec)
444
- const args = asObject(rawArgs, 'args')
445
- assertNoIdentityArgs(args)
446
- return groupPort().groupSpawnBySession(exec.sessionId, {
447
- name: asString(args.name, 'name'),
448
- brief: optionalString(args.brief, 'brief'),
449
- })
450
- })
451
- },
452
- },
453
- {
454
- name: 'hams_append_evidence',
455
- description: '为工作项追加证据;证据只增不改,更正请另起一条并指向被替代行。',
456
- parameters: {
457
- type: 'object',
458
- properties: {
459
- id: { type: 'string' },
460
- kind: { type: 'string', description: 'reading / photo / receipt / log / doc' },
461
- ref: { type: 'string', description: '证据引用路径或标识' },
462
- observedAt: { type: 'number', description: '事实发生时间(UTC 毫秒)' },
463
- source: { type: 'string' },
464
- },
465
- required: ['id', 'kind', 'ref', 'observedAt'],
466
- additionalProperties: false,
467
- },
468
- execute(rawArgs, exec) {
469
- return run(() => {
470
- const thread = identity(exec)
471
- const args = asObject(rawArgs, 'args')
472
- assertNoIdentityArgs(args)
473
- const workItemId = asString(args.id, 'id')
474
- ownedItemCheck(thread, workItemId)
475
- const runRow = db
476
- .prepare(
477
- `SELECT id FROM task_run WHERE work_item_id = ? AND member_id = ?
478
- ORDER BY created_at DESC LIMIT 1`,
479
- )
480
- .get(workItemId, thread.memberId) as { id: string } | undefined
481
- return service.appendEvidence(context(thread, exec, 'append_evidence'), {
482
- workItemId,
483
- kind: asString(args.kind, 'kind'),
484
- ref: asString(args.ref, 'ref'),
485
- observedAt: asNumber(args.observedAt, 'observedAt'),
486
- source: optionalString(args.source, 'source'),
487
- runId: runRow?.id ?? null,
488
- })
489
- })
490
- },
491
- },
492
- {
493
- name: 'hams_submit_result',
494
- description: '结束本轮运行并提交结果摘要;工作项会进入复查或等待验收。',
495
- parameters: {
496
- type: 'object',
497
- properties: { id: { type: 'string' }, summary: { type: 'string' } },
498
- required: ['id', 'summary'],
499
- additionalProperties: false,
500
- },
501
- execute(rawArgs, exec) {
502
- return run(() => {
503
- const thread = identity(exec)
504
- const args = asObject(rawArgs, 'args')
505
- assertNoIdentityArgs(args)
506
- const workItemId = asString(args.id, 'id')
507
- ownedItemCheck(thread, workItemId)
508
- const current = db
509
- .prepare(
510
- `SELECT id FROM task_run WHERE work_item_id = ? AND member_id = ? AND status = 'running'
511
- ORDER BY created_at DESC LIMIT 1`,
512
- )
513
- .get(workItemId, thread.memberId) as { id: string } | undefined
514
- if (!current) throw new HamsError('STATE_INVALID', '当前没有运行中的任务;先认领并开始运行')
515
- return service.submitResult(context(thread, exec, 'submit_result'), {
516
- runId: current.id,
517
- summary: asString(args.summary, 'summary'),
518
- })
519
- })
520
- },
521
- },
522
- {
523
- name: 'hams_evaluate_check',
524
- description: '对到期复查做判定;服务端会按证据清单复核,不采信模型自述。',
525
- parameters: {
526
- type: 'object',
527
- properties: { checkId: { type: 'string' } },
528
- required: ['checkId'],
529
- additionalProperties: false,
530
- },
531
- execute(rawArgs, exec) {
532
- return run(() => {
533
- const thread = identity(exec)
534
- const args = asObject(rawArgs, 'args')
535
- assertNoIdentityArgs(args)
536
- const checkId = asString(args.checkId, 'checkId')
537
- const row = db
538
- .prepare(
539
- `SELECT c.work_item_id AS work_item_id, w.assignee_id AS assignee_id
540
- FROM work_item_check c JOIN work_item w ON w.id = c.work_item_id
541
- WHERE c.id = ?`,
542
- )
543
- .get(checkId) as { work_item_id: string; assignee_id: string | null } | undefined
544
- if (!row) throw new HamsError('STATE_INVALID', `复查不存在:${checkId}`)
545
- if (row.assignee_id !== thread.memberId) {
546
- throw new HamsError('POLICY_DENIED', '只能判定自己名下的复查')
547
- }
548
- return service.evaluateCheck(context(thread, exec, 'evaluate_check'), { checkId })
549
- })
550
- },
551
- },
552
- {
553
- name: 'hams_report_blocker',
554
- description: '上报阻塞,并给出阻塞类别与说明。',
555
- parameters: {
556
- type: 'object',
557
- properties: {
558
- id: { type: 'string' },
559
- category: {
560
- type: 'string',
561
- enum: ['needs_evidence', 'external', 'dependency', 'permission', 'human_input', 'data_source'],
562
- },
563
- note: { type: 'string' },
564
- },
565
- required: ['id', 'category', 'note'],
566
- additionalProperties: false,
567
- },
568
- execute(rawArgs, exec) {
569
- return run(() => {
570
- const thread = identity(exec)
571
- const args = asObject(rawArgs, 'args')
572
- assertNoIdentityArgs(args)
573
- const workItemId = asString(args.id, 'id')
574
- ownedItemCheck(thread, workItemId)
575
- return service.reportBlocker(context(thread, exec, 'report_blocker'), {
576
- workItemId,
577
- category: asString(args.category, 'category'),
578
- note: asString(args.note, 'note', { allowEmpty: true }),
579
- })
580
- })
581
- },
582
- },
583
- {
584
- name: 'hams_request_closure',
585
- description: '请示关单:服务端核对证据清单与验证器后生成人工审批。not 直接关单。',
586
- parameters: {
587
- type: 'object',
588
- properties: { id: { type: 'string' }, reason: { type: 'string' } },
589
- required: ['id', 'reason'],
590
- additionalProperties: false,
591
- },
592
- execute(rawArgs, exec) {
593
- return run(() => {
594
- const thread = identity(exec)
595
- const args = asObject(rawArgs, 'args')
596
- assertNoIdentityArgs(args)
597
- const workItemId = asString(args.id, 'id')
598
- ownedItemCheck(thread, workItemId)
599
- return service.requestClosure(context(thread, exec, 'request_closure'), {
600
- workItemId,
601
- reason: asString(args.reason, 'reason'),
602
- })
603
- })
604
- },
605
- },
606
- {
607
- name: 'hams_propose',
608
- description: '提出想做的事(建单 / 升级 / 权限 / 范围 / 知识变更),由人决定;提议不改变工作项。',
609
- parameters: {
610
- type: 'object',
611
- properties: {
612
- kind: {
613
- type: 'string',
614
- enum: ['create_task', 'upgrade_task', 'permission_request', 'scope_change', 'knowledge_change'],
615
- },
616
- subjectKey: { type: 'string', description: 'project:<id> 或 work_item:<id> 或 global' },
617
- dedupeKey: { type: 'string' },
618
- title: { type: 'string' },
619
- body: { type: 'string' },
620
- rationale: {
621
- type: 'object',
622
- description: '必须包含 evidence 数组(可以是引用,但不能为空)',
623
- properties: { evidence: { type: 'array' }, expectedEffect: { type: 'string' }, cost: {} },
624
- required: ['evidence'],
625
- },
626
- },
627
- required: ['kind', 'subjectKey', 'dedupeKey', 'title', 'body', 'rationale'],
628
- additionalProperties: false,
629
- },
630
- execute(rawArgs, exec) {
631
- return run(() => {
632
- const member = memberIdentity(exec)
633
- const args = asObject(rawArgs, 'args')
634
- assertNoIdentityArgs(args)
635
- const subjectKey = asString(args.subjectKey, 'subjectKey')
636
- const deciderId = resolveDecider(db, subjectKey)
637
- const rationale = asObject(args.rationale, 'rationale')
638
- const evidence = rationale.evidence
639
- if (!Array.isArray(evidence) || evidence.length === 0) {
640
- throw new HamsError('POLICY_DENIED', '提议必须带证据:rationale.evidence 不能为空')
641
- }
642
- return service.propose(context(member, exec, 'propose'), {
643
- kind: asString(args.kind, 'kind') as
644
- | 'create_task'
645
- | 'upgrade_task'
646
- | 'permission_request'
647
- | 'scope_change'
648
- | 'knowledge_change',
649
- subjectKey,
650
- dedupeKey: asString(args.dedupeKey, 'dedupeKey'),
651
- deciderId,
652
- title: asString(args.title, 'title'),
653
- body: asString(args.body, 'body'),
654
- rationale: {
655
- evidence,
656
- expectedEffect: optionalString(rationale.expectedEffect, 'rationale.expectedEffect'),
657
- },
658
- })
659
- })
660
- },
661
- },
662
- ]
663
-
664
- return tools
665
- }
666
-
667
- /** 提议的决定人由服务端解析:项目负责人 / 工作项负责人;global 不允许模型发起。 */
668
- export function resolveDecider(db: DatabaseSync, subjectKey: string): string {
669
- if (subjectKey.startsWith('project:')) {
670
- const projectId = subjectKey.slice('project:'.length)
671
- const row = db.prepare('SELECT owner_human_id FROM project WHERE id = ?').get(projectId) as
672
- | { owner_human_id: string }
673
- | undefined
674
- if (row) return row.owner_human_id
675
- }
676
- if (subjectKey.startsWith('work_item:')) {
677
- const workItemId = subjectKey.slice('work_item:'.length)
678
- const row = db.prepare('SELECT owner_human_id FROM work_item WHERE id = ?').get(workItemId) as
679
- | { owner_human_id: string }
680
- | undefined
681
- if (row) return row.owner_human_id
682
- }
683
- throw new HamsError('POLICY_DENIED', '提议必须挂在项目或工作项上,由对应负责人决定')
684
- }
685
-
686
- export const TOOL_NAMES = [
687
- 'hams_list_my_work',
688
- 'hams_get_work_item',
689
- 'hams_claim',
690
- 'hams_assign_task',
691
- 'hams_list_project_work',
692
- 'hams_group_members',
693
- 'hams_group_send',
694
- 'hams_remember',
695
- 'hams_group_spawn_bot',
696
- 'hams_append_evidence',
697
- 'hams_submit_result',
698
- 'hams_evaluate_check',
699
- 'hams_report_blocker',
700
- 'hams_request_closure',
701
- 'hams_propose',
702
- ] as const
703
-
704
- /** 明确不存在的工具:审批、提议决定、直接关单、任意 SQL。 */
705
- export const FORBIDDEN_TOOL_NAMES = [
706
- 'hams_decide_approval',
707
- 'hams_decide_proposal',
708
- 'hams_close_work_item',
709
- 'hams_sql',
710
- 'hams_http',
711
- ] as const