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,276 +0,0 @@
1
- /** Real DSH agent lifecycle. Business rows only hold host session references; no transcript IO. */
2
- import { randomUUID } from 'node:crypto'
3
- import path from 'node:path'
4
- import { mkdir } from 'node:fs/promises'
5
- import type { DatabaseSync } from 'node:sqlite'
6
- import { HamsError } from '../domain/errors.ts'
7
- import type { HamsService } from '../service/hams.ts'
8
- import type { CommandContext, CommandReceipt, RunRow, ThreadRow, WorkItemRow } from '../service/types.ts'
9
- import { CAPABILITY, requireCapability, requireHuman, resolvePrincipal } from '../service/permissions.ts'
10
- import { TOOL_NAMES } from '../service/tools.ts'
11
-
12
- export interface HostAgent {
13
- session: { id: string }
14
- status?: string
15
- followup(message: unknown): void
16
- cancel(cause: { kind: 'user' }, options: { keepInbox: true }): unknown
17
- }
18
- export interface HostAgentHandle { agent: HostAgent; dispose(): unknown | Promise<unknown> }
19
- export interface HostAgents {
20
- create(options: { sessionId: string; meta: Record<string, unknown>; agentOptions: Record<string, unknown>; setup?: unknown }): Promise<HostAgentHandle>
21
- resume(options: { resumeSessionId: string; agentOptions: Record<string, unknown>; setup?: unknown }): Promise<HostAgentHandle>
22
- }
23
- export interface BotConfig { memberId: string; version: number; agentPreset: string; model: string | null }
24
- export interface SessionAdapterOptions {
25
- db: DatabaseSync
26
- service: HamsService
27
- agents: HostAgents
28
- dataDir: string
29
- /** SDK createUserMessage, injected so tests never import/start the host runtime. */
30
- createUserMessage(input: { content: { type: 'text'; text: string }[]; source: { kind: 'user' } }): unknown
31
- /** Must resolve/mount the named preset, installModelSelection, and bind configured model. */
32
- compose(config: BotConfig): Promise<{ agentOptions: Record<string, unknown>; setup?: unknown; agentPreset?: string | null }>
33
- /** 成员配置里没写 preset 时的兜底(HAMS 自带 hams-bot)。 */
34
- defaultPreset?: string
35
- now?: () => number
36
- timeZone?: string
37
- sessionId?: () => string
38
- }
39
- export interface RunFacts {
40
- stateAndEpoch: unknown
41
- evidenceAndHashes: unknown
42
- taskAndCriteria: unknown
43
- approvalsAndBlockers: unknown
44
- budgetAndCapabilities: unknown
45
- }
46
-
47
- /** Only the SDK-owned Agent session is trusted. Flat sessionId and model arguments are ignored. */
48
- export function hostToolContext(exec: unknown): { sessionId: string; callId?: string } {
49
- const value = exec as { agent?: { session?: { id?: unknown } }; callId?: unknown } | undefined
50
- const id = value?.agent?.session?.id
51
- return { sessionId: typeof id === 'string' ? id : '', ...(typeof value?.callId === 'string' ? { callId: value.callId } : {}) }
52
- }
53
-
54
- export function buildRunFacts(db: DatabaseSync, runId: string, now = Date.now(), timeZone = 'Asia/Shanghai'): RunFacts {
55
- const run = db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow | undefined
56
- if (!run) throw new HamsError('STATE_INVALID', `运行不存在:${runId}`)
57
- const item = db.prepare('SELECT * FROM work_item WHERE id=?').get(run.work_item_id) as WorkItemRow
58
- const template = item.template_version_id ? db.prepare('SELECT * FROM work_template_version WHERE id=?').get(item.template_version_id) : null
59
- const date = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(now))
60
- const utc = new Date(`${date}T00:00:00Z`)
61
- utc.setUTCDate(utc.getUTCDate() - ((utc.getUTCDay() + 6) % 7))
62
- const week = utc.toISOString().slice(0, 10)
63
- const budgets = item.project_id ? db.prepare(`SELECT period_kind,period_start,metric,used,limit_value,
64
- MAX(0,limit_value-used) AS remaining FROM project_budget WHERE project_id=?
65
- AND ((period_kind='day' AND period_start=?) OR (period_kind='week' AND period_start=?)) ORDER BY period_kind,metric`).all(item.project_id, date, week) : []
66
- return {
67
- stateAndEpoch: { workItemId: item.id, status: item.status, revision: item.revision, executionEpoch: item.execution_epoch, fence: run.fence, runtimeEpoch: run.runtime_epoch },
68
- evidenceAndHashes: { evidenceSetHash: item.evidence_set_hash, evidence: db.prepare("SELECT id,kind,ref,content_hash,observed_at,source FROM evidence WHERE work_item_id=? AND status='active' ORDER BY id").all(item.id) },
69
- taskAndCriteria: { runId, trigger: run.trigger, checkId: run.check_id, title: item.title, memberVersion: run.member_version, templateVersion: template ?? null },
70
- approvalsAndBlockers: { blockedReason: item.blocked_reason, approvals: db.prepare("SELECT id,status,reason_text,expires_at FROM approval WHERE subject_kind='work_item_close' AND subject_id=? AND status='pending' ORDER BY id").all(item.id) },
71
- budgetAndCapabilities: {
72
- budgets,
73
- permissions: db.prepare('SELECT capability,scope FROM member_permission WHERE member_id=? ORDER BY capability,scope').all(run.member_id),
74
- // 政策白名单为空 ≠ 没有工具:HAMS 工具是全局注册的,真实可用。把两者分开写,避免 bot 误判「tools 为空所以干不了」。
75
- toolPolicy: db.prepare('SELECT tool_name FROM member_tool_policy WHERE member_id=? AND enabled=1 ORDER BY tool_name').all(run.member_id),
76
- hamsTools: [...TOOL_NAMES],
77
- toolPolicyNote: 'toolPolicy 是额外白名单;hamsTools 一定可用。宿主另有全局工具(如 working_directory)。',
78
- },
79
- }
80
- }
81
-
82
- export class HamsSessionAdapter {
83
- private options: SessionAdapterOptions
84
- private handles = new Map<string, Promise<HostAgentHandle>>()
85
- private locks = new Map<string, Promise<unknown>>()
86
- private delivered = new Set<string>()
87
- private closed = false
88
- constructor(options: SessionAdapterOptions) { this.options = options }
89
-
90
- private serial<T>(key: string, fn: () => Promise<T>): Promise<T> {
91
- const next = (this.locks.get(key) ?? Promise.resolve()).catch(() => {}).then(() => {
92
- if (this.closed) throw new HamsError('HOST_CAPABILITY_MISSING', '会话适配器已停止')
93
- return fn()
94
- })
95
- this.locks.set(key, next)
96
- void next.finally(() => { if (this.locks.get(key) === next) this.locks.delete(key) }).catch(() => {})
97
- return next
98
- }
99
- private thread(workItemId: string): ThreadRow | undefined {
100
- return this.options.db.prepare("SELECT * FROM work_item_thread WHERE work_item_id=? AND status IN ('active','pending_rotate') ORDER BY generation DESC LIMIT 1").get(workItemId) as ThreadRow | undefined
101
- }
102
- private config(memberId: string, version?: number): BotConfig {
103
- const row = this.options.db.prepare(`SELECT v.*,m.status,m.kind FROM member_version v JOIN member m ON m.id=v.member_id WHERE v.member_id=? AND v.version=${version === undefined ? 'm.current_version' : '?'}`).get(...(version === undefined ? [memberId] : [memberId, version])) as { version: number; agent_preset: string; model: string | null; kind: string; status: string } | undefined
104
- if (!row || row.kind !== 'bot' || row.status !== 'active') throw new HamsError('HOST_CAPABILITY_MISSING', 'bot 没有有效且已发布的宿主 preset')
105
- return { memberId, version: row.version, agentPreset: row.agent_preset || this.options.defaultPreset || '', model: row.model }
106
- }
107
- private needsRotation(thread: ThreadRow): boolean {
108
- return thread.status === 'pending_rotate' || Boolean(this.options.db.prepare("SELECT 1 FROM member_version WHERE member_id=? AND version>? AND change_class='major'").get(thread.member_id, thread.member_version))
109
- }
110
- private async open(config: BotConfig, existing?: string, cwdKey?: string): Promise<HostAgentHandle> {
111
- if (existing && this.handles.has(existing)) return this.handles.get(existing)!
112
- const id = existing ?? (this.options.sessionId ?? (() => `session-${randomUUID()}`))()
113
- if (!id.trim() || this.handles.has(id)) throw new HamsError('STATE_INVALID', '宿主会话 id 重复或为空')
114
- const pending = (async () => {
115
- const composition = await this.options.compose(config)
116
- const { agentPreset: composedPreset, ...rest } = composition
117
- const presetId = composedPreset ?? config.agentPreset
118
- // 一个 bot 可能同时是多个工作项的负责人:工作项各占一个子目录,不和别的任务混在一起。
119
- const cwd = path.resolve(this.options.dataDir, 'bots', config.memberId, 'work', String(cwdKey || 'default').replace(/[^a-zA-Z0-9._-]+/g, '-') || 'default')
120
- const base = path.resolve(this.options.dataDir, 'bots') + path.sep
121
- if (!cwd.startsWith(base)) throw new HamsError('POLICY_DENIED', 'bot 工作目录越界')
122
- await mkdir(cwd, { recursive: true })
123
- const handle = existing
124
- ? await this.options.agents.resume({ resumeSessionId: id, ...rest })
125
- : await this.options.agents.create({ sessionId: id, meta: { cwd, ...(presetId ? { agentPreset: presetId } : {}) }, ...rest })
126
- if (handle.agent.session.id !== id) { await handle.dispose(); throw new HamsError('HOST_CAPABILITY_MISSING', '宿主返回的会话身份与请求不一致') }
127
- return handle
128
- })()
129
- this.handles.set(id, pending)
130
- try { return await pending } catch (error) { this.handles.delete(id); throw error }
131
- }
132
- async release(sessionId: string): Promise<void> {
133
- const pending = this.handles.get(sessionId)
134
- if (!pending) return
135
- // Keep the entry until disposal completes: another open must not race the writer teardown.
136
- try { await (await pending).dispose() } finally { this.handles.delete(sessionId) }
137
- }
138
- async cancel(sessionId: string): Promise<{ accepted: true }> {
139
- const thread = this.options.db.prepare('SELECT * FROM work_item_thread WHERE session_id=?').get(sessionId) as ThreadRow | undefined
140
- if (!thread) throw new HamsError('POLICY_DENIED', '会话未绑定 HAMS 工作项')
141
- const handle = await this.open(this.config(thread.member_id, thread.member_version), sessionId, thread.work_item_id)
142
- await handle.agent.cancel({ kind: 'user' }, { keepInbox: true })
143
- return { accepted: true }
144
- }
145
- async claim(ctx: CommandContext, input: { workItemId: string }): Promise<CommandReceipt> {
146
- return this.serial(input.workItemId, async () => {
147
- const { service } = this.options
148
- const item = service.getWorkItem(ctx, input.workItemId)
149
- if (item.status !== 'ready') throw new HamsError('STATE_INVALID', '只有 ready 的工作项可以认领')
150
- const principal = resolvePrincipal(this.options.db, ctx.principalId, this.now())
151
- // 系统派发替被指派的 bot 认领:execute 就够(与 service.claim 的 system 分支一致)。
152
- if (ctx.channel === 'system' && !principal.capabilities.has(CAPABILITY.workItemClaim)) {
153
- requireCapability(principal, CAPABILITY.workItemExecute)
154
- } else {
155
- requireCapability(principal, CAPABILITY.workItemClaim)
156
- }
157
- const thread = this.thread(item.id)
158
- const fresh = !thread || this.needsRotation(thread) || thread.member_id !== ctx.principalId
159
- const handle = await this.open(this.config(ctx.principalId, fresh ? undefined : thread!.member_version), fresh ? undefined : thread!.session_id, item.id)
160
- try {
161
- const receipt = service.claim(ctx, { workItemId: item.id, sessionId: handle.agent.session.id })
162
- if (thread && thread.session_id !== handle.agent.session.id) await this.release(thread.session_id)
163
- return receipt
164
- } catch (error) { if (fresh) await this.release(handle.agent.session.id); throw error }
165
- })
166
- }
167
- async startRun(runId: string, text = ''): Promise<{ sessionId: string; runId: string; accepted: true }> {
168
- const run = this.options.db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow | undefined
169
- if (!run) throw new HamsError('STATE_INVALID', `运行不存在:${runId}`)
170
- return this.serial(run.work_item_id, () => this.start(runId, text))
171
- }
172
- private async start(runId: string, text: string) {
173
- const { db, service } = this.options
174
- const run = db.prepare('SELECT * FROM task_run WHERE id=?').get(runId) as RunRow
175
- let thread = db.prepare('SELECT * FROM work_item_thread WHERE id=?').get(run.thread_id) as ThreadRow
176
- if (this.delivered.has(runId)) return { sessionId: thread.session_id, runId, accepted: true as const }
177
- if (run.status !== 'queued') throw new HamsError('STATE_INVALID', '只能投递 queued 运行;结果不明时不自动重发')
178
- const rotate = this.needsRotation(thread)
179
- const oldId = thread.session_id
180
- const handle = await this.open(this.config(run.member_id, rotate ? undefined : thread.member_version), rotate ? undefined : oldId, run.work_item_id)
181
- try {
182
- service.startRun({ principalId: run.member_id, channel: 'system', correlationId: `host-start:${runId}` }, { runId, sessionId: handle.agent.session.id })
183
- thread = this.thread(run.work_item_id)!
184
- const facts = buildRunFacts(db, runId, this.now(), this.options.timeZone)
185
- handle.agent.followup(this.options.createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: `HAMS 数据库事实快照;本轮以此为准。\n${JSON.stringify(facts)}\n本轮指令:${text || '推进本轮任务,并通过 HAMS 工具提交结果或阻塞。'}` }] }))
186
- this.delivered.add(runId)
187
- if (rotate) await this.release(oldId)
188
- return { sessionId: thread.session_id, runId, accepted: true as const }
189
- } catch (error) {
190
- // Admission may have partially succeeded. Never label a host delivery failure as success/retry it blindly.
191
- db.prepare("UPDATE task_run SET status='unknown',finished_at=?,error_kind='host_delivery_unknown' WHERE id=? AND status='running'").run(this.now(), runId)
192
- if (rotate && this.thread(run.work_item_id)?.session_id !== handle.agent.session.id) await this.release(handle.agent.session.id)
193
- throw error
194
- }
195
- }
196
- async rotateThread(ctx: CommandContext, input: { workItemId: string; reason?: string }): Promise<CommandReceipt> {
197
- return this.serial(input.workItemId, async () => {
198
- const item = this.options.service.getWorkItem(ctx, input.workItemId)
199
- this.authorize(ctx)
200
- if (!item.allowedActions.includes('rotateThread') || !item.assignee_id) throw new HamsError('STATE_INVALID', '当前工作项不能换新会话')
201
- const old = this.thread(item.id)
202
- const handle = await this.open(this.config(item.assignee_id), undefined, item.id)
203
- try {
204
- const result = this.options.service.rotateThread(ctx, { ...input, sessionId: handle.agent.session.id })
205
- if (old) await this.release(old.session_id)
206
- return result
207
- } catch (error) { await this.release(handle.agent.session.id); throw error }
208
- })
209
- }
210
- async instruct(ctx: CommandContext, input: { workItemId: string; text: string }): Promise<unknown> {
211
- return this.serial(input.workItemId, async () => {
212
- this.authorize(ctx)
213
- if (!input.text?.trim()) throw new HamsError('STATE_INVALID', '追加指令不能为空')
214
- const item = this.options.service.getWorkItem(ctx, input.workItemId)
215
- if (['completed','cancelled','failed','paused'].includes(item.status)) throw new HamsError('STATE_INVALID', '当前工作项不接受追加指令')
216
- const run = this.options.db.prepare("SELECT * FROM task_run WHERE work_item_id=? AND status IN ('queued','running') ORDER BY created_at DESC LIMIT 1").get(item.id) as RunRow | undefined
217
- if (!run) throw new HamsError('STATE_INVALID', '没有待执行或正在运行的轮次;请先重试或等待复查')
218
- if (run.status === 'queued') return this.start(run.id, input.text)
219
- const thread = this.thread(item.id)
220
- if (!thread || thread.status !== 'active') throw new HamsError('STATE_INVALID', '当前会话已等待换代')
221
- const handle = await this.open(this.config(thread.member_id, thread.member_version), thread.session_id, item.id)
222
- handle.agent.followup(this.options.createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: `HAMS 最新事实:\n${JSON.stringify(buildRunFacts(this.options.db, run.id, this.now(), this.options.timeZone))}\n追加指令:${input.text}` }] }))
223
- return { sessionId: thread.session_id, runId: run.id, accepted: true }
224
- })
225
- }
226
- /**
227
- * 指派型任务:ready + 有 bot 负责人 + 没有排队/在跑的轮次 → 替它认领并投递这一轮。
228
- * 这是「bot 给 bot 分派任务」的落地点:协调 bot 只负责指派,真正的执行在被指派 bot 自己的会话里。
229
- */
230
- async dispatchAssignments(limit = 10): Promise<{ started: number; errors: string[] }> {
231
- const rows = this.options.db
232
- .prepare(
233
- `SELECT i.id, i.assignee_id FROM work_item i JOIN member m ON m.id = i.assignee_id
234
- WHERE i.status = 'ready' AND m.kind = 'bot' AND m.status = 'active'
235
- AND NOT EXISTS (SELECT 1 FROM task_run r WHERE r.work_item_id = i.id AND r.status IN ('queued','running'))
236
- ORDER BY i.updated_at LIMIT ?`,
237
- )
238
- .all(limit) as { id: string; assignee_id: string }[]
239
- const report = { started: 0, errors: [] as string[] }
240
- for (const row of rows) {
241
- try {
242
- const receipt = await this.claim({ principalId: row.assignee_id, channel: 'system', correlationId: `assign:${row.id}` }, { workItemId: row.id })
243
- const runId = receipt.detail && typeof receipt.detail === 'object' ? (receipt.detail as { runId?: string }).runId : undefined
244
- if (runId) await this.startRun(runId)
245
- report.started++
246
- } catch (error) {
247
- report.errors.push(`${row.id}: ${this.errorText(error)}`)
248
- }
249
- }
250
- return report
251
- }
252
-
253
- private errorText(error: unknown): string {
254
- if (error && typeof error === 'object' && 'code' in error) return `${(error as { code?: string }).code}: ${(error as { message?: string }).message ?? ''}`
255
- return String(error)
256
- }
257
-
258
- /** Call after each business tick. No recovery of unknown/running deliveries. */
259
- async dispatchQueued(limit = 20): Promise<{ started: number; errors: string[] }> {
260
- const rows = this.options.db.prepare("SELECT id FROM task_run WHERE status='queued' ORDER BY created_at LIMIT ?").all(limit) as { id: string }[]
261
- const report = { started: 0, errors: [] as string[] }
262
- for (const row of rows) try { await this.startRun(row.id); report.started++ } catch (error) { report.errors.push(`${row.id}: ${String(error)}`) }
263
- return report
264
- }
265
- private authorize(ctx: CommandContext) {
266
- const principal = resolvePrincipal(this.options.db, ctx.principalId, this.now())
267
- requireHuman(principal); requireCapability(principal, CAPABILITY.workItemManage)
268
- if (ctx.channel !== 'ui') throw new HamsError('POLICY_DENIED', '人工会话操作只接受 UI 通道')
269
- }
270
- private now() { return (this.options.now ?? Date.now)() }
271
- async dispose(): Promise<void> {
272
- this.closed = true
273
- await Promise.allSettled([...this.locks.values()])
274
- for (const id of [...this.handles.keys()]) await this.release(id)
275
- }
276
- }