@arcfold/dsh 0.0.0-stage → 0.4.0

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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/cordis.patch.yml +138 -0
  5. package/package.json +70 -4
  6. package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
  7. package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
  8. package/presets/arcfold.patch.yml +110 -0
  9. package/runtime/README.md +19 -0
  10. package/runtime/bootstrap.json +15 -0
  11. package/runtime/desktop-python.tar.gz +0 -0
  12. package/runtime/host/package.json +11 -0
  13. package/runtime/host/pnpm-workspace.yaml +9 -0
  14. package/runtime/initial-policy.json +94 -0
  15. package/runtime/profile/pnpm-workspace.yaml +7 -0
  16. package/scripts/exact-package-patch.mjs +106 -0
  17. package/scripts/patch-deepseek-transport.mjs +14 -0
  18. package/src/agent.mjs +56 -0
  19. package/src/browse-scope.mjs +14 -0
  20. package/src/budget.mjs +369 -0
  21. package/src/business.mjs +334 -0
  22. package/src/context-material.mjs +119 -0
  23. package/src/deepseek-budget-plugin.mjs +58 -0
  24. package/src/deepseek-provider.mjs +57 -0
  25. package/src/deepseek-receipt.mjs +97 -0
  26. package/src/deepseek-transport.mjs +97 -0
  27. package/src/desktop/adapter.mjs +16 -0
  28. package/src/desktop/bootstrap.mjs +84 -0
  29. package/src/desktop/configuration.mjs +117 -0
  30. package/src/desktop/provider.mjs +46 -0
  31. package/src/desktop/python-runtime.mjs +105 -0
  32. package/src/desktop/setup-service.mjs +81 -0
  33. package/src/desktop/vendor/LICENSE.deepseek +21 -0
  34. package/src/desktop/vendor/NOTICE.md +5 -0
  35. package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
  36. package/src/evidence-ownership.mjs +51 -0
  37. package/src/evidence.mjs +35 -0
  38. package/src/goal-context.mjs +52 -0
  39. package/src/goal-execution-tool.mjs +74 -0
  40. package/src/goal-execution.mjs +333 -0
  41. package/src/goal-tool.mjs +56 -0
  42. package/src/goal-work-control.mjs +63 -0
  43. package/src/goal-work.mjs +257 -0
  44. package/src/goals.mjs +578 -0
  45. package/src/importer.mjs +298 -0
  46. package/src/index.mjs +53 -0
  47. package/src/learning-contracts.mjs +220 -0
  48. package/src/learning-defaults.mjs +98 -0
  49. package/src/learning-goals.mjs +165 -0
  50. package/src/learning-plugin.mjs +28 -0
  51. package/src/learning-runner.mjs +275 -0
  52. package/src/learning.mjs +534 -0
  53. package/src/legacy-export.mjs +332 -0
  54. package/src/legacy-import-source.mjs +356 -0
  55. package/src/metered-provider-host.mjs +101 -0
  56. package/src/metered-transport.mjs +435 -0
  57. package/src/method-context.mjs +57 -0
  58. package/src/method-lifecycle.mjs +34 -0
  59. package/src/method-tool.mjs +61 -0
  60. package/src/methods.mjs +425 -0
  61. package/src/model-selection.mjs +41 -0
  62. package/src/native-context-call.mjs +88 -0
  63. package/src/native-continuation.mjs +217 -0
  64. package/src/native-request-authority.mjs +90 -0
  65. package/src/openai-models.mjs +4 -0
  66. package/src/openai-receipt.mjs +98 -0
  67. package/src/openai-transport.mjs +167 -0
  68. package/src/openai-usage.mjs +194 -0
  69. package/src/opportunities.mjs +295 -0
  70. package/src/preparation-artifacts.mjs +213 -0
  71. package/src/preparation-plugin.mjs +290 -0
  72. package/src/runtime-command.mjs +264 -0
  73. package/src/runtime-config.mjs +417 -0
  74. package/src/runtime-guard.mjs +284 -0
  75. package/src/runtime-host.mjs +212 -0
  76. package/src/runtime-init.mjs +278 -0
  77. package/src/runtime-installation.mjs +518 -0
  78. package/src/runtime-lifecycle.mjs +227 -0
  79. package/src/runtime-model-settings.mjs +111 -0
  80. package/src/runtime-readiness.mjs +285 -0
  81. package/src/runtime-workspace.mjs +18 -0
  82. package/src/session-bridge.mjs +365 -0
  83. package/src/store.mjs +394 -0
  84. package/src/tree/LICENSE +21 -0
  85. package/src/tree/attribution.mjs +120 -0
  86. package/src/tree/clusters.mjs +61 -0
  87. package/src/tree/consolidation.mjs +103 -0
  88. package/src/tree/context.mjs +211 -0
  89. package/src/tree/contracts.mjs +142 -0
  90. package/src/tree/experience.mjs +140 -0
  91. package/src/tree/experiment-budget.mjs +84 -0
  92. package/src/tree/hooks.mjs +254 -0
  93. package/src/tree/host-context.mjs +107 -0
  94. package/src/tree/host-process.mjs +288 -0
  95. package/src/tree/host.mjs +37 -0
  96. package/src/tree/improvement.mjs +106 -0
  97. package/src/tree/items.mjs +294 -0
  98. package/src/tree/jobs.mjs +184 -0
  99. package/src/tree/learning.mjs +202 -0
  100. package/src/tree/metrics.mjs +203 -0
  101. package/src/tree/model-owner.mjs +254 -0
  102. package/src/tree/observations.mjs +73 -0
  103. package/src/tree/prompts.mjs +500 -0
  104. package/src/tree/reports.mjs +67 -0
  105. package/src/tree/repository.mjs +236 -0
  106. package/src/tree/runtime.mjs +745 -0
  107. package/src/tree/script-writer.mjs +149 -0
  108. package/src/tree/scripts.mjs +208 -0
  109. package/src/tree/signals.mjs +118 -0
  110. package/src/tree/snapshots.mjs +379 -0
  111. package/src/tree/token-counter.mjs +75 -0
  112. package/src/tree/token-counter.py +37 -0
  113. package/src/tree/tokenizer-resources/LICENSE +21 -0
  114. package/src/tree/tokenizer-resources/README.md +31 -0
  115. package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
  116. package/src/tree/tokenizer-resources/manifest.json +61 -0
  117. package/src/tree/tokenizer-resources/native.pyi +1042 -0
  118. package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
  119. package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
  120. package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
  121. package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
  122. package/src/tree/tools.mjs +99 -0
  123. package/src/tree/tree.mjs +202 -0
  124. package/src/tree/ui.mjs +285 -0
  125. package/src/tree/verification.mjs +143 -0
  126. package/src/understandings.mjs +309 -0
  127. package/src/web-budget-recovery.mjs +181 -0
@@ -0,0 +1,51 @@
1
+ import { constants, existsSync, fstatSync, realpathSync, statSync } from 'node:fs';
2
+ import { mkdir, open, stat } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+
5
+ const held = new WeakMap();
6
+ export function canonicalEvidenceDirectory(directory) {
7
+ if (typeof directory !== 'string' || !isAbsolute(directory) || directory.includes('\0'))
8
+ throw new Error('An absolute evidence directory is required');
9
+ let ancestor = resolve(directory); const tail = [];
10
+ while (!existsSync(ancestor)) {
11
+ const parent = dirname(ancestor); tail.unshift(relative(parent, ancestor)); ancestor = parent;
12
+ }
13
+ return join(realpathSync(ancestor), ...tail);
14
+ }
15
+
16
+ /** One existing kernel lock, shared by provider startup and owned initialization. */
17
+ export async function ownEvidenceDirectory(directory) {
18
+ if (process.platform === 'win32') throw new Error('The metered provider currently requires a POSIX host');
19
+ directory = canonicalEvidenceDirectory(directory);
20
+ await mkdir(directory, { recursive: true, mode: 0o700 });
21
+ const { tryLockExclusive } = await import('@deepseek-ai/node-addon-system/flock');
22
+ const path = join(directory, 'owner.lock');
23
+ for (let attempt = 0; attempt < 3; attempt++) {
24
+ const handle = await open(path, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
25
+ try {
26
+ await tryLockExclusive(handle.fd);
27
+ const current = await stat(path, { bigint: true }).catch(error => {
28
+ if (error.code === 'ENOENT') return null; throw error;
29
+ });
30
+ const info = await handle.stat({ bigint: true });
31
+ if (current && info.ino === current.ino && info.dev === current.dev) {
32
+ const record = { handle, path, directory, closed: false };
33
+ const owner = Object.freeze({ directory, async close() {
34
+ if (record.closed) return; record.closed = true; await handle.close();
35
+ } });
36
+ held.set(owner, record); return owner;
37
+ }
38
+ } catch (error) { await handle.close(); throw error; }
39
+ await handle.close();
40
+ }
41
+ throw new Error('The transport evidence directory changed while acquiring ownership');
42
+ }
43
+
44
+ export function assertEvidenceOwnership(owner, directory) {
45
+ const record = held.get(owner);
46
+ if (!record || record.closed || record.directory !== canonicalEvidenceDirectory(directory))
47
+ throw new Error('A live evidence-directory lease is required');
48
+ const info = fstatSync(record.handle.fd, { bigint: true }), current = statSync(record.path, { bigint: true });
49
+ if (info.ino !== current.ino || info.dev !== current.dev)
50
+ throw new Error('The held evidence-directory lease was replaced');
51
+ }
@@ -0,0 +1,35 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
4
+ export const unavailable = row => !row || row.deleted || row.withdrawn === true || row.useProhibited || ['withdrawn', 'superseded', 'rejected', 'paused'].includes(row.status);
5
+ const atSequence = (row, cutoff) => Number.isSafeInteger(row?.createdSequence) && Number.isSafeInteger(row.updatedSequence) && row.createdSequence <= cutoff && row.updatedSequence <= cutoff;
6
+ export const sourceText = row => typeof row.content === 'string' ? row.content : (row.message?.content || []).filter(block => block.type === 'text').map(block => block.text).join('\n');
7
+ // Provider replay state belongs to the original native message/transport, not
8
+ // to quoted evidence. Keep its persisted original and sourceRef hash untouched;
9
+ // only omit the known opaque field from the model-facing provenance copy.
10
+ export function evidenceProvenance(provenance) {
11
+ if (provenance?.kind === 'dsh_session' && provenance.source?.kind === 'model') {
12
+ const { replayState, ...source } = provenance.source;
13
+ return structuredClone({ ...provenance, source });
14
+ }
15
+ return structuredClone(provenance ?? null);
16
+ }
17
+ export const sourceAvailable = (store, row) => !unavailable(row) && !(store.get('source_controls', row.id)?.useProhibited || store.get('source_controls', row.id)?.withdrawn);
18
+
19
+ // A record never outlives its originals or the exact dependency revisions it read.
20
+ export function dependenciesCurrent(store, row, cutoff, path = new Set()) {
21
+ if (unavailable(row) || !atSequence(row, cutoff) || path.has(row.id)) return false;
22
+ const next = new Set([...path, row.id]);
23
+ for (const ref of row.dependencies || []) {
24
+ const kind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
25
+ if (!['events', 'understandings', 'methods'].includes(kind)) return false;
26
+ const dependency = store.get(kind, ref.id);
27
+ if (unavailable(dependency) || dependency.revision !== ref.revision || !atSequence(dependency, cutoff)) return false;
28
+ if (kind === 'events' && !sourceAvailable(store, dependency)) return false;
29
+ if (kind !== 'events' && !dependenciesCurrent(store, dependency, cutoff, next)) return false;
30
+ }
31
+ return Array.isArray(row.sourceIds) && row.sourceIds.length > 0 && row.sourceIds.every(id => {
32
+ const source = store.get('events', id);
33
+ return sourceAvailable(store, source) && atSequence(source, cutoff);
34
+ });
35
+ }
@@ -0,0 +1,52 @@
1
+ import { hash } from './evidence.mjs';
2
+ import { canonicalJson } from './store.mjs';
3
+
4
+ export const GOAL_CONTEXT = 'arcfold/linked-goal';
5
+ export const GOAL_TOOL = 'arcfold_link_goal';
6
+ const INTRO = '以下长期目标已由本轮主助手关联,程序已核对其版本与原件。本次贡献是助手的判断,不是用户认可或目标已实现的证明。联系只解释当前工作,不扩大委托或工具权限;暂停或结束追踪的目标只能在用户明确选回时讨论,不因此恢复追踪。来源失效时仅可依本次新原话重新澄清,不使用无效旧内容。\n';
7
+
8
+ export function goalContextMessage(bundle) {
9
+ return { id: 'arcfold-goal-context-' + hash(bundle), role: 'user',
10
+ source: { kind: GOAL_CONTEXT, sessionId: bundle.sessionId, turn: bundle.turn,
11
+ startSeq: bundle.startSeq, linkIds: bundle.links.map(row => row.linkId) },
12
+ content: [{ type: 'text', text: INTRO + JSON.stringify(bundle) }] };
13
+ }
14
+
15
+ function receiptOf(event) {
16
+ if (event?.type !== 'tool/result' || event.data.message?.isError !== false || event.data.error ||
17
+ event.data.message.role !== 'tool' || event.data.message.source?.kind !== 'tool' ||
18
+ event.data.message.source.callId !== event.data.message.toolCallId) return null;
19
+ const blocks = event.data.message.content;
20
+ if (!Array.isArray(blocks) || blocks.some(block => block.type !== 'text')) return null;
21
+ try { return JSON.parse(blocks.map(block => block.text).join('')); } catch { return null; }
22
+ }
23
+
24
+ // The link core is immutable. A later native call can return the same link only
25
+ // for the exact same selection; derive its success from actual call/result pairs.
26
+ export function hasSuccessfulGoalReceipt(events, link) {
27
+ return events.filter(event => event.seq > link.startSeq && event.type === 'tool/call' &&
28
+ event.data.turn === link.turn && event.data.name === GOAL_TOOL).some(call => {
29
+ if (events.filter(event => event.type === 'tool/call' && event.data.callId === call.data.callId).length !== 1)
30
+ return false;
31
+ let args;
32
+ try { args = JSON.parse(call.data.arguments); } catch { return false; }
33
+ if (!args || Object.keys(args).length !== 1 || !Object.hasOwn(args, 'selection') ||
34
+ canonicalJson(args.selection) !== canonicalJson(link.selection)) return false;
35
+ const results = events.filter(event => event.seq > call.seq && event.type === 'tool/result' &&
36
+ event.data.turn === link.turn && event.data.step === call.data.step &&
37
+ event.data.message?.toolCallId === call.data.callId);
38
+ if (results.length !== 1) return false;
39
+ const receipt = receiptOf(results[0]);
40
+ return receipt?.linkId === link.id && receipt.goal?.id === link.goalId &&
41
+ receipt.goal.revision === link.goalRevision && receipt.goal.definitionVersion === link.definitionVersion &&
42
+ receipt.goal.contentHash === link.contentHash;
43
+ });
44
+ }
45
+
46
+ export function eligibleGoalBundle(goals, events, position) {
47
+ const bundle = goals.currentBundle(position);
48
+ return { ...bundle, links: bundle.links.filter(entry => {
49
+ const link = goals.store.get('goal_links', entry.linkId);
50
+ return link && hasSuccessfulGoalReceipt(events, link);
51
+ }) };
52
+ }
@@ -0,0 +1,74 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
3
+ import { resolveNativeContextCall, belongsToSelectionBatch } from './native-context-call.mjs';
4
+ import { STAGE_TOOLS } from './goal-execution.mjs';
5
+
6
+ export const name = 'arcfold-goal-execution';
7
+ export const inject = ['tools', 'agents', 'goals', 'arcfoldBridge', 'arcfoldWork', 'systemPrompt'];
8
+ const reject = message => { throw new HarnessError(message, 'ARCFOLD_STAGE_CONTROL_INVALID'); };
9
+ const string = description => ({ type: 'string', required: true, description });
10
+ const reference = { goal_id: string('Current native work id returned by get_goal.'), revision: { type: 'integer', required: true } };
11
+ const definition = {
12
+ objective: string('The concrete currently delegated stage, bounded by the current user request.'),
13
+ doneWhen: string('Observable results that complete this stage; reaching these ends its automatic execution.'),
14
+ boundaries: string('Scope, exclusions, and decisions that require waiting for the user.'),
15
+ max_goal_rounds: { type: 'integer', description: 'Optional automatic rounds for this stage, default 3, maximum 128.' },
16
+ };
17
+
18
+ export function apply(ctx) {
19
+ const work = ctx.arcfoldWork;
20
+ work.attach(ctx.root);
21
+ ctx.on('goal/changed', ({ agent, change }) => work.observeNative(agent, change));
22
+ ctx.on('agent/inbox/inserted', ({ agent, message }) => work.ownerArrived(agent, message));
23
+ ctx.on('agent/turn-stopping', ({ agent, turn }) => work.stopping(agent, turn));
24
+ ctx.tools.guard(exec => {
25
+ if (['create_goal', 'update_goal'].includes(exec.name)) return 'Use Arcfold stage controls; native goal mutation cannot authorize execution.';
26
+ try {
27
+ if (STAGE_TOOLS.some(name => belongsToSelectionBatch(exec, name))) {
28
+ const events = exec.agent?.session.snapshotEvents() ?? [];
29
+ const call = events.findLast(e => e.type === 'tool/call' && e.data.callId === exec.callId);
30
+ const assistant = call && events.findLast(e => e.seq < call.seq && e.type === 'assistant/message');
31
+ if (assistant?.data.message.content.filter(block => block.type === 'tool-call').length !== 1)
32
+ return 'Call Arcfold work control alone, then read its receipt before any other action.';
33
+ }
34
+ if (exec.agent) work.assertCurrent(exec.agent, { tools: true, name: exec.name });
35
+ } catch (error) { return error.message; }
36
+ });
37
+ ctx.systemPrompt.section({ name: 'arcfold:execution-control', order: 52, interpolate: false,
38
+ text: `Arcfold 统一目标推进,DSH 负责执行当前明确委托的阶段。长期方向、当前阶段和本轮步骤分别维护:已有目标与学习提供背景,todo_write展示本轮操作,阶段工具落实继续或等待。你是作这些判断的同一位主助手。
39
+ 用户只讨论方向、要求建立长期目标或共同拆解时,先交流并让后台整理长期目标。只有当前原话已委托可独立持续推进的具体工作,才单独调用 arcfold_start_work,写清当前阶段、可检查的完成条件、范围及须等待用户的决定。完成条件面向眼前阶段,避免把整个长期愿景作为当前执行范围。
40
+ 本轮行动完成后,按实际结果单独调用 arcfold_decide_work。当前阶段的完成条件已满足时用 complete,stageDone=true、needsUserDecision=false;后续阶段即使还需共同讨论,也先结束已经完成的当前阶段并保留长期方向。完成当前阶段仍需用户判断或当前应停下时用 wait;仅当同一阶段还有明确、可独立完成的工作时用 continue,并写清下一步。普通文字提问也要落实为 wait。提交决定后交代本轮结果。没有明确继续决定,程序会等待新输入。
41
+ 阶段修改依据本轮新用户要求,用 arcfold_revise_work;修改作废旧许可并保持停止,继续时再用 arcfold_resume_work。恢复只针对已有具体阶段。旧版本只有长期执行目标而缺少阶段时,先根据新的用户要求明确阶段。get_goal可查看现有执行和阶段。
42
+ 每次continue只许可下一轮;结果仍需核实,完成步骤不等于长期目标达成。阶段控制的内容判断由你负责,程序核对原话、来源、版本及许可。后台学习更新理解与进展,不签发执行许可。` });
43
+ const output = { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] };
44
+ ctx.tools.register(defineTool({ name: 'get_goal', description: 'Read current delegated execution and its Arcfold stage; this does not start work.',
45
+ parameters: {}, output, execute(_args, exec) {
46
+ if (!exec.agent || ctx.agents.get(exec.agent.id) !== exec.agent) reject('A live main session is required.');
47
+ return work.receipt(exec.agent);
48
+ }, presentCall: () => ({ card: 'generic', title: '查看当前阶段', kind: 'read' }) }));
49
+ for (const [name, revise] of [['arcfold_start_work', false], ['arcfold_revise_work', true]]) {
50
+ ctx.tools.register(defineTool({ name,
51
+ description: revise ? 'Revise the current bounded stage from a new direct user request. Invalidates continuation and keeps execution stopped until explicitly resumed. Call alone.'
52
+ : 'Start one concrete stage explicitly delegated by the current user. A long-term goal or discussion alone is not a running stage. Define completion and user-decision boundaries. Call alone; no automatic next round without arcfold_decide_work.',
53
+ parameters: { ...(revise ? reference : {}), ...definition }, output,
54
+ async execute(args, exec) {
55
+ const call = await resolveNativeContextCall(ctx, exec, reject);
56
+ if (call.batch.length !== 1) reject('Stage control must be a standalone tool call.');
57
+ exec.signal.throwIfAborted();
58
+ return work.start(call, args, revise);
59
+ }, presentCall: () => ({ card: 'generic', title: revise ? '调整当前阶段' : '明确当前阶段', kind: 'other' }) }));
60
+ }
61
+ ctx.tools.register(defineTool({ name: 'arcfold_decide_work',
62
+ description: 'After this turn\'s work, decide whether the current stage is complete, needs the user, or has a concrete independent next step. continue grants only the next round of the same stage. Call alone as your last action, then explain results.',
63
+ parameters: { ...reference, action: { type: 'string', required: true, enum: ['continue', 'wait', 'complete'] },
64
+ reason: string('Evidence and reasoning from this turn for the decision.'),
65
+ stageDone: { type: 'boolean', required: true, description: 'Whether this stage\'s completion conditions are met.' },
66
+ needsUserDecision: { type: 'boolean', required: true, description: 'Whether a user choice or information is still required to complete this CURRENT stage. Set false once this stage is complete, even when future stages await discussion.' },
67
+ nextStep: string('Concrete independent next step within this same stage for continue; empty for wait/complete.') }, output,
68
+ async execute(args, exec) {
69
+ const call = await resolveNativeContextCall(ctx, exec, reject);
70
+ if (call.batch.length !== 1) reject('Stage control must be a standalone tool call.');
71
+ exec.signal.throwIfAborted();
72
+ return work.decision(call, args);
73
+ }, presentCall: () => ({ card: 'generic', title: '确定下一步', kind: 'other' }) }));
74
+ }
@@ -0,0 +1,333 @@
1
+ import { canonicalJson, DomainError } from './store.mjs';
2
+ import { hash, sourceAvailable, sourceText, dependenciesCurrent } from './evidence.mjs';
3
+ import { nativeDefinitionHash, workId, workRelation } from './goal-work.mjs';
4
+
5
+ const fail = message => { throw new DomainError('stage_control_required', message); };
6
+ const same = (a, b) => canonicalJson(a) === canonicalJson(b);
7
+ const owner = message => message?.role === 'user' && message.source?.kind === 'user' && message.source.principal === undefined;
8
+ const stageId = (sessionId, goalId) => 'execution-stage-' + hash([sessionId, goalId]);
9
+ const ref = goal => ({ id: goal.id, revision: goal.revision });
10
+ const currentTurn = events => events.findLast(event => event.type === 'turn/start');
11
+ const goalInput = (events, start) => events.find(event => event.seq > start.seq && event.type === 'user/message' && event.data.source?.kind === 'goal');
12
+ export const STAGE_TOOLS = ['arcfold_start_work', 'arcfold_revise_work', 'arcfold_decide_work', 'arcfold_resume_work'];
13
+
14
+ /** Arcfold owns a bounded stage and one-round permission; DSH remains the only scheduler. */
15
+ export class GoalExecution {
16
+ constructor({ business }) { this.store = business.storage; this.mutations = new WeakMap(); this.controls = new WeakMap(); }
17
+ attach(root) { this.root = root; }
18
+ native(agent) { return this.root?.get('goals')?.get(agent) ?? null; }
19
+ current(agent) {
20
+ const goal = this.native(agent);
21
+ return goal ? this.store.get('execution_stages', stageId(agent.session.id, goal.id)) : null;
22
+ }
23
+ view(stage) {
24
+ if (!stage) return null;
25
+ try { this.assertSources(stage); } catch {
26
+ return { id: stage.id, status: 'needs_review', nativeGoalId: stage.nativeGoalId, nativeRevision: stage.nativeRevision,
27
+ reason: '阶段依据已经变化,等待重新明确当前工作。', available: false };
28
+ }
29
+ return { id: stage.id, definitionVersion: stage.definitionVersion, epoch: stage.epoch,
30
+ objective: stage.objective, doneWhen: stage.doneWhen, boundaries: stage.boundaries,
31
+ status: stage.status, reason: stage.reason, nextStep: stage.permit?.nextStep ?? '',
32
+ nativeGoalId: stage.nativeGoalId, nativeRevision: stage.nativeRevision, goalRef: stage.goalRef,
33
+ sources: stage.sourceRefs.map(reference => {
34
+ const original = this.store.get('events', reference.id);
35
+ return { id: original.id, role: original.role, content: sourceText(original), receivedSeq: reference.receivedSeq };
36
+ }), available: true,
37
+ authority: 'arcfold-current-stage; native-execution-within-this-boundary' };
38
+ }
39
+ receipt(agent, extra = {}) { return { goal: this.native(agent), stage: this.view(this.current(agent)), ...extra }; }
40
+ save(stage, changes) { return this.store.save('execution_stages', { ...stage, ...changes }, { expectedRevision: stage.revision }); }
41
+ mutate(agent, operation, fn) {
42
+ if (this.mutations.has(agent)) fail('已有阶段控制正在处理。');
43
+ this.mutations.set(agent, operation);
44
+ try { return fn(); } finally { this.mutations.delete(agent); }
45
+ }
46
+ disarm(agent) { this.root.get('goals').disarm(agent); }
47
+ stoppedAfter(agent) {
48
+ const stop = agent.session.snapshotEvents().findLast(event => event.type === 'goal/change' &&
49
+ ['pause', 'block', 'clear'].includes(event.data.operation));
50
+ return Math.max(stop?.seq ?? -1, this.current(agent)?.stopBoundary ?? -1);
51
+ }
52
+ beginControl(agent) {
53
+ if (this.controls.has(agent)) fail('当前工作控制正在确认,请等待原请求的结果。');
54
+ let resolve;
55
+ const promise = new Promise(done => { resolve = done; });
56
+ this.controls.set(agent, promise);
57
+ return () => { this.controls.delete(agent); resolve(); };
58
+ }
59
+ async awaitControl(agent) { await this.controls.get(agent); }
60
+ abortControl(agent, decisionId) {
61
+ this.disarm(agent);
62
+ const stage = this.current(agent);
63
+ if (stage?.permit?.userDecision === decisionId) this.save(stage, { status: 'waiting', permit: null, admission: null,
64
+ reason: '恢复结果尚待确认,执行保持停止。', stopBoundary: agent.session.seq - 1 });
65
+ }
66
+ sources(call, after = -1) {
67
+ const refs = [];
68
+ for (const event of call.events.filter(e => e.seq > call.startSeq && e.seq < call.call.seq && e.type === 'user/message' && owner(e.data))) {
69
+ const received = call.events.findLast(e => e.type === 'agent/inbox/spliced' && e.seq < event.seq &&
70
+ e.data.inserted?.some(message => same(message, event.data)));
71
+ if (!received || received.seq <= after) continue;
72
+ const originals = this.store.listAll('events', { where: { sessionId: call.sessionId } }).filter(row =>
73
+ row.message?.id === event.data.id && same(row.message, event.data));
74
+ if (originals.length !== 1 || !sourceAvailable(this.store, originals[0])) continue;
75
+ refs.push({ id: originals[0].id, revision: originals[0].revision, messageHash: hash(event.data), receivedSeq: received.seq });
76
+ }
77
+ if (!refs.length) fail('明确或恢复当前阶段需要本轮新收到的用户原话。');
78
+ return refs;
79
+ }
80
+ assertSources(stage) {
81
+ if (!stage?.sourceRefs?.length) fail('这项旧工作还需要明确当前阶段,请根据用户本次要求重新整理。');
82
+ for (const source of stage.sourceRefs) {
83
+ const row = this.store.get('events', source.id);
84
+ if (!sourceAvailable(this.store, row) || row.revision !== source.revision || hash(row.message) !== source.messageHash)
85
+ fail('当前阶段的委托原话已经撤回或改变。');
86
+ }
87
+ if (stage.goalRef) {
88
+ const goal = this.store.get('goals', stage.goalRef.id);
89
+ if (!goal || goal.status !== 'tracking' || goal.definitionVersion !== stage.goalRef.definitionVersion ||
90
+ !dependenciesCurrent(this.store, { ...goal, status: 'active' }, this.store.currentSequence()))
91
+ fail('关联的长期目标已经变化,需根据当前要求重新明确阶段。');
92
+ }
93
+ }
94
+ assertNative(stage, goal) {
95
+ this.assertSources(stage);
96
+ if (!goal || goal.id !== stage.nativeGoalId || goal.revision !== stage.nativeRevision || nativeDefinitionHash(goal) !== stage.nativeDefinitionHash)
97
+ fail('执行版本已经变化,需要通过 Arcfold 重新明确当前阶段。');
98
+ }
99
+ definition(args) {
100
+ for (const key of ['objective', 'doneWhen', 'boundaries'])
101
+ if (typeof args[key] !== 'string' || !args[key].trim() || args[key].length > 3000) fail('当前阶段需要具体任务、完成条件和推进边界。');
102
+ const max = args.max_goal_rounds ?? 3;
103
+ if (!Number.isSafeInteger(max) || max < 1 || max > 128) fail('阶段续跑轮数应为1到128的整数。');
104
+ return { objective: args.objective.trim(), doneWhen: args.doneWhen.trim(), boundaries: args.boundaries.trim(), maxGoalRounds: max };
105
+ }
106
+ start(call, args, revise = false) {
107
+ const spec = this.definition(args), sourceRefs = this.sources(call, this.stoppedAfter(call.agent));
108
+ const before = this.native(call.agent), previous = this.current(call.agent);
109
+ if (revise && before?.phase === 'complete') fail('上一阶段已经完成,请用 arcfold_start_work 开始用户新委托的阶段。');
110
+ const roundsAtStart = revise ? before?.roundsStarted ?? 0 : 0;
111
+ const nativeRoundLimit = roundsAtStart + spec.maxGoalRounds;
112
+ if (nativeRoundLimit > 128) fail('本次阶段轮数超出宿主剩余轮数,请缩小当前阶段。');
113
+ if (revise && (!before || before.id !== args.goal_id || before.revision !== args.revision)) fail('修改须对应当前执行的编号和版本。');
114
+ if (!revise && before && before.phase !== 'complete') fail('已有工作需要通过 arcfold_revise_work 明确新阶段。');
115
+ if (previous && ['waiting', 'paused', 'needs_review', 'completed'].includes(previous.status)) this.sources(call, previous.stopBoundary ?? -1);
116
+ const link = this.store.listAll('goal_links', { where: { sessionId: call.sessionId, startSeq: call.startSeq } })[0];
117
+ const parent = link && this.store.get('goals', link.goalId);
118
+ const goalRef = parent?.status === 'tracking' ? { id: parent.id, definitionVersion: link.definitionVersion } : null;
119
+ this.assertSources({ sourceRefs, goalRef });
120
+ const goal = this.mutate(call.agent, revise ? 'edit' : 'create', () => revise
121
+ ? this.root.get('goals').edit(call.agent, ref(before), { objective: spec.objective, maxGoalRounds: nativeRoundLimit })
122
+ : this.root.get('goals').create(call.agent, { objective: spec.objective, maxGoalRounds: nativeRoundLimit }));
123
+ try {
124
+ const id = stageId(call.sessionId, goal.id), old = this.store.get('execution_stages', id);
125
+ this.store.save('execution_stages', { id, sessionId: call.sessionId, nativeGoalId: goal.id,
126
+ nativeRevision: goal.revision, nativeDefinitionHash: nativeDefinitionHash(goal), ...spec, roundsAtStart,
127
+ definitionVersion: (old?.definitionVersion ?? 0) + 1, epoch: (old?.epoch ?? 0) + 1,
128
+ sourceRefs, goalRef, status: revise ? 'waiting' : 'running',
129
+ reason: revise ? '当前阶段已修改,等待明确继续。' : '执行当前明确委托的阶段。',
130
+ stopBoundary: revise ? call.call.seq : null, decisionId: null, permit: null, admission: null,
131
+ origin: { callId: call.callId, turn: call.turn, callSeq: call.call.seq, operation: revise ? 'revise' : 'start' } },
132
+ { expectedRevision: old?.revision ?? null });
133
+ if (revise) this.disarm(call.agent);
134
+ } catch (error) { this.disarm(call.agent); throw error; }
135
+ return this.receipt(call.agent);
136
+ }
137
+ decision(call, args) {
138
+ let stage = this.current(call.agent); const goal = this.native(call.agent);
139
+ this.assertNative(stage, goal);
140
+ if (goal.id !== args.goal_id || goal.revision !== args.revision || stage.status !== 'running' || goal.activation !== 'armed')
141
+ fail('决定须对应正在执行的当前阶段与版本。');
142
+ if (this.decisionFor(stage, call.turn)) fail('本轮已经作出推进决定,请交代结果。');
143
+ if (args.action === 'continue' && [...call.agent.inbox.nextStep, ...call.agent.inbox.nextTurn].some(owner))
144
+ fail('先处理新收到的用户表达,再决定下一轮。');
145
+ if (!['continue', 'wait', 'complete'].includes(args.action) || typeof args.reason !== 'string' || !args.reason.trim() ||
146
+ args.reason.length > 2000 || typeof args.stageDone !== 'boolean' || typeof args.needsUserDecision !== 'boolean' ||
147
+ typeof args.nextStep !== 'string' || args.nextStep.length > 2000) fail('阶段决定需要结果依据、完成判断及是否需用户决定。');
148
+ if (args.action === 'continue' && (args.stageDone || args.needsUserDecision || !args.nextStep.trim() || goal.roundsStarted >= goal.maxGoalRounds))
149
+ fail('只有当前阶段尚未完成、无需用户决定且下一步明确时,才可许可下一轮。');
150
+ if (args.action === 'complete' && (!args.stageDone || args.needsUserDecision)) fail('请核对当前阶段的完成条件:满足时 complete 要求 stageDone=true 且 needsUserDecision=false;完成本阶段仍需信息或选择时用 wait。后续阶段的讨论另行保留。');
151
+ if (args.action !== 'continue' && args.nextStep !== '') fail('等待或完成不携带下一轮执行步骤。');
152
+ const id = 'stage-decision-' + hash([call.sessionId, call.callId]);
153
+ if (this.store.get('stage_decisions', id)) fail('此阶段决定已经记录,不能重复执行。');
154
+ const decision = { id, stageId: stage.id, sessionId: call.sessionId, definitionVersion: stage.definitionVersion,
155
+ epoch: stage.epoch, nativeGoalId: goal.id, nativeRevision: goal.revision, turn: call.turn, callId: call.callId,
156
+ callSeq: call.call.seq, action: args.action, reason: args.reason, stageDone: args.stageDone,
157
+ needsUserDecision: args.needsUserDecision, nextStep: args.nextStep, evidence: 'current-main-model-decision; semantic-judgment-not-proven-by-schema' };
158
+ this.store.save('stage_decisions', decision, { expectedRevision: null });
159
+ if (args.action === 'continue') {
160
+ this.save(stage, { decisionId: id, reason: args.reason, permit: { decisionId: id, nativeRevision: goal.revision,
161
+ round: goal.roundsStarted + 1, fromTurn: call.turn, nextStep: args.nextStep, issuedSeq: call.call.seq } });
162
+ } else {
163
+ const operation = args.action === 'wait' ? 'pause' : 'complete';
164
+ const after = this.mutate(call.agent, operation, () => this.root.get('goals')[operation](call.agent, ref(goal)));
165
+ try {
166
+ stage = this.current(call.agent);
167
+ this.save(stage, { nativeRevision: after.revision, status: args.action === 'wait' ? 'waiting' : 'completed',
168
+ reason: args.reason, decisionId: id, permit: null, stopBoundary: call.call.seq });
169
+ } catch (error) { this.disarm(call.agent); throw error; }
170
+ }
171
+ return this.receipt(call.agent, { decisionId: id });
172
+ }
173
+ decisionFor(stage, turn) {
174
+ const decision = stage?.decisionId && this.store.get('stage_decisions', stage.decisionId);
175
+ return decision?.turn === turn && decision.definitionVersion === stage.definitionVersion && decision.epoch === stage.epoch ? decision : null;
176
+ }
177
+ success(events, decision) {
178
+ return Boolean(decision && events.some(event => event.type === 'tool/result' && event.data.message?.toolCallId === decision.callId &&
179
+ event.data.message.isError === false && event.data.message.content?.some(block => {
180
+ if (block.type !== 'text') return false;
181
+ try { const value = JSON.parse(block.text); return value.decisionId === decision.id && value.stage?.id === decision.stageId; } catch { return false; }
182
+ })));
183
+ }
184
+ /** A browser or a new owner turn may resume the same stage, never revive an old ticket. */
185
+ resume(agent, { goalId, revision, sourceRefs = null, userDecision = null, currentTurn: turn = null, callId = null, callSeq = null, allowRevisedTurn = false }) {
186
+ let stage = this.current(agent); const goal = this.native(agent);
187
+ this.assertNative(stage, goal);
188
+ if (goal.id !== goalId || goal.revision !== revision || stage.status === 'completed' ||
189
+ goal.roundsStarted >= goal.maxGoalRounds || goal.phase === 'complete') fail('该阶段不能直接恢复,请重新明确当前工作。');
190
+ if (!sourceRefs?.length && !userDecision) fail('恢复需要新的用户决定。');
191
+ if (sourceRefs && !sourceRefs.some(source => source.receivedSeq > (stage.stopBoundary ?? -1)) && !allowRevisedTurn)
192
+ fail('恢复依据必须在停下之后新收到。');
193
+ const after = this.mutate(agent, 'resume', () => this.root.get('goals').resume(agent, ref(goal)));
194
+ try {
195
+ stage = this.current(agent);
196
+ const epoch = stage.epoch + 1;
197
+ this.save(stage, { nativeRevision: after.revision, epoch, status: 'running', reason: '按用户的新决定继续当前阶段。',
198
+ sourceRefs: sourceRefs ? [...stage.sourceRefs, ...sourceRefs.filter(source => !stage.sourceRefs.some(old => old.id === source.id))] : stage.sourceRefs,
199
+ decisionId: null, admission: null, permit: userDecision ? { nativeRevision: after.revision, round: after.roundsStarted + 1,
200
+ fromTurn: null, nextStep: stage.objective, issuedSeq: agent.session.seq - 1, userDecision } : null,
201
+ resumedBy: sourceRefs ? { kind: 'owner-input', sourceIds: sourceRefs.map(source => source.id), turn, callId, callSeq } : { kind: 'browser', userDecision } });
202
+ } catch (error) { this.disarm(agent); throw error; }
203
+ return this.receipt(agent);
204
+ }
205
+ observeNative(agent, change) {
206
+ if (this.mutations.get(agent) === change.operation) return;
207
+ const stage = this.current(agent) ?? (change.cleared?.id && this.store.get('execution_stages', stageId(agent.session.id, change.cleared.id)));
208
+ // Native controls may always stop execution. They cannot grant Arcfold stage authority.
209
+ this.disarm(agent);
210
+ if (stage) this.save(stage, { status: ['pause', 'block', 'clear'].includes(change.operation) ? 'paused' : 'needs_review',
211
+ reason: ['pause', 'block'].includes(change.operation) ? '当前执行已停下。' : '执行定义或控制已变化,需重新明确当前阶段。',
212
+ nativeRevision: change.goal?.revision ?? change.cleared?.revision ?? stage.nativeRevision, epoch: stage.epoch + 1,
213
+ permit: null, admission: null, decisionId: null, stopBoundary: agent.session.seq - 1 });
214
+ }
215
+ ownerArrived(agent, message) {
216
+ if (!owner(message)) return;
217
+ const stage = this.current(agent);
218
+ if (stage && (stage.permit || stage.decisionId)) this.save(stage, { permit: null, decisionId: null,
219
+ reason: '先处理用户的新表达,再决定是否继续。' });
220
+ }
221
+ ownerEntered(agent, messages) {
222
+ if (messages.some(owner)) this.ownerArrived(agent, messages.find(owner));
223
+ }
224
+ async stopping(agent, turn) {
225
+ const goal = this.native(agent), stage = this.current(agent);
226
+ if (!goal || goal.phase !== 'active') return;
227
+ const events = agent.session.snapshotEvents(), decision = this.decisionFor(stage, turn);
228
+ try {
229
+ this.assertNative(stage, goal);
230
+ if (stage.status === 'running' && stage.permit?.fromTurn === turn && stage.permit.nativeRevision === goal.revision &&
231
+ stage.permit.round === goal.roundsStarted + 1 && decision?.action === 'continue' && this.success(events, decision)) return;
232
+ } catch { /* The existing result remains delivered; invalid continuation stops. */ }
233
+ this.disarm(agent);
234
+ if (stage && stage.status === 'running') this.save(stage, { status: 'waiting', permit: null, admission: null,
235
+ reason: '本轮已结束,等待明确下一步。', stopBoundary: agent.session.seq - 1 });
236
+ }
237
+ admit(agent, messages, turn) {
238
+ const automatic = messages.find(message => message.source?.kind === 'goal');
239
+ if (!automatic) return;
240
+ const goal = this.native(agent), stage = this.current(agent), source = automatic.source;
241
+ try {
242
+ this.assertNative(stage, goal);
243
+ if (stage.admission?.turn === turn && stage.admission.messageId === automatic.id) return;
244
+ const permit = stage.permit;
245
+ if (source.round <= 0 || stage.status !== 'running' || goal.phase !== 'active' || goal.activation !== 'armed' ||
246
+ !permit || source.goalId !== goal.id || source.revision !== goal.revision || permit.nativeRevision !== goal.revision ||
247
+ permit.round !== source.round || source.round !== goal.roundsStarted + 1) fail('下一轮缺少当前阶段的单次继续许可。');
248
+ if (permit.decisionId && !this.success(agent.session.snapshotEvents(), this.store.get('stage_decisions', permit.decisionId)))
249
+ fail('继续决定缺少真实成功回执。');
250
+ if (permit.userDecision) {
251
+ const control = this.store.get('goal_work_controls', permit.userDecision);
252
+ if (control?.state !== 'applied' || control.request.action !== 'resume' ||
253
+ control.request.sessionId !== stage.sessionId || control.request.nativeGoalId !== goal.id ||
254
+ control.request.nativeRevision + 1 !== goal.revision) fail('网页恢复决定尚未确认生效。');
255
+ }
256
+ this.save(stage, { permit: null, decisionId: null, admission: { turn, messageId: automatic.id, round: source.round,
257
+ nativeRevision: source.revision, definitionVersion: stage.definitionVersion, epoch: stage.epoch,
258
+ decisionId: permit.decisionId ?? null, userDecision: permit.userDecision ?? null } });
259
+ } catch (error) { this.disarm(agent); throw error; }
260
+ }
261
+ controlSucceeded(events, control, stage) {
262
+ return Boolean(control?.callId && events.some(event => event.type === 'tool/result' &&
263
+ event.data.message?.toolCallId === control.callId && event.data.message.isError === false &&
264
+ event.data.message.content?.some(block => {
265
+ try { const result = JSON.parse(block.text); return result.stage?.id === stage.id &&
266
+ result.stage.definitionVersion === stage.definitionVersion; } catch { return false; }
267
+ })));
268
+ }
269
+ ownerTransition(agent) {
270
+ const events = agent.session.snapshotEvents(), start = currentTurn(events), stage = this.current(agent);
271
+ if (!start || !stage) return false;
272
+ const control = stage.resumedBy?.kind === 'owner-input' && stage.resumedBy.turn === start.data.turn
273
+ ? stage.resumedBy : stage.origin?.turn === start.data.turn ? stage.origin : null;
274
+ return this.controlSucceeded(events, control, stage);
275
+ }
276
+ freshOwner(agent) {
277
+ const events = agent.session.snapshotEvents(), start = currentTurn(events), stage = this.current(agent);
278
+ if (!start) return false;
279
+ const boundary = Math.max(stage?.origin?.callSeq ?? -1, stage?.resumedBy?.callSeq ?? -1, this.stoppedAfter(agent));
280
+ return events.some(event => event.seq > start.seq && event.type === 'user/message' && owner(event.data) &&
281
+ events.some(received => received.type === 'agent/inbox/spliced' && received.seq > boundary && received.seq < event.seq &&
282
+ received.data.inserted?.some(message => same(message, event.data))) &&
283
+ this.store.listAll('events', { where: { sessionId: agent.session.id } }).some(row =>
284
+ same(row.message, event.data) && sourceAvailable(this.store, row)));
285
+ }
286
+ historicalContinuation(agent) { return this.ownerTransition(agent) || this.freshOwner(agent); }
287
+ browserControlSaved(agent, id, request) {
288
+ const stage = this.store.get('execution_stages', stageId(agent.session.id, request.nativeGoalId));
289
+ return Boolean(stage?.resumedBy?.kind === 'browser' && stage.resumedBy.userDecision === id &&
290
+ stage.nativeRevision >= request.nativeRevision + 1);
291
+ }
292
+ boundToTurn(stage, turn) {
293
+ return Boolean(stage && (stage.origin?.turn === turn || stage.admission?.turn === turn ||
294
+ stage.resumedBy?.kind === 'owner-input' && stage.resumedBy.turn === turn));
295
+ }
296
+ assertCurrent(agent, { tools = false, name = null } = {}) {
297
+ const events = agent.session.snapshotEvents(), start = currentTurn(events);
298
+ if (!start) return;
299
+ const input = goalInput(events, start), stage = this.current(agent), decision = this.decisionFor(stage, start.data.turn);
300
+ const repair = ['get_goal', 'arcfold_start_work', 'arcfold_revise_work', 'arcfold_resume_work'].includes(name);
301
+ // A newly entered owner correction may inspect/redefine the stage. It does
302
+ // not authorize ordinary actions under an obsolete automatic admission.
303
+ if (this.freshOwner(agent) && (!tools || repair)) return;
304
+ if (tools && decision) fail('本轮已作出阶段决定,请交代结果,后续行动交由下一轮执行。');
305
+ const expected = this.store.listAll('context_snapshots', { where: { sessionId: agent.session.id, turn: start.data.turn } })[0]?.material.executionStage;
306
+ const boundOwner = this.boundToTurn(stage, start.data.turn);
307
+ if (!input && !boundOwner && !expected?.boundToTurn) return;
308
+ if (expected?.boundToTurn && expected.id !== stage?.id) fail('发起本次模型请求的阶段已被替换,旧动作不能继续执行。');
309
+ const goal = this.native(agent), admission = stage?.admission;
310
+ this.assertNative(stage, goal);
311
+ if (!tools && decision && ['wait', 'complete'].includes(decision.action) && this.success(events, decision)) return;
312
+ if (this.ownerTransition(agent)) {
313
+ if (!tools || repair || stage.status === 'running' && goal.phase === 'active' && goal.activation === 'armed') return;
314
+ fail('当前阶段已停下,先按用户要求恢复再执行。');
315
+ }
316
+ if (!input) fail('当前阶段控制缺少成功回执。');
317
+ if (!admission || admission.turn !== start.data.turn || admission.messageId !== input.data.id ||
318
+ admission.epoch !== stage.epoch || admission.definitionVersion !== stage.definitionVersion)
319
+ fail('此自动轮不再持有当前阶段的执行许可。');
320
+ if (stage.status !== 'running' || goal.phase !== 'active' || goal.activation !== 'armed' ||
321
+ admission.nativeRevision !== goal.revision || admission.round !== goal.roundsStarted)
322
+ fail('当前阶段已停下或改变,旧自动轮不能继续执行。');
323
+ }
324
+ stopForGoal(goalId) {
325
+ for (const stage of this.store.listAll('execution_stages').filter(row => row.status === 'running' &&
326
+ (row.goalRef?.id === goalId || workRelation(this.store, workId(row.sessionId, row.nativeGoalId))?.goalId === goalId))) {
327
+ const agent = this.root?.get('agents')?.get(stage.sessionId);
328
+ if (!agent || this.native(agent)?.id !== stage.nativeGoalId) continue;
329
+ const goal = this.native(agent);
330
+ if (goal.phase === 'active') this.root.get('goals').pause(agent, ref(goal));
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,56 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
3
+ import { resolveNativeContextCall, belongsToSelectionBatch } from './native-context-call.mjs';
4
+ import { GOAL_TOOL, goalContextMessage } from './goal-context.mjs';
5
+
6
+ export const name = 'arcfold-goal-tool';
7
+ export const inject = ['tools', 'agents', 'arcfoldBridge', 'arcfoldGoals'];
8
+ const reject = message => { throw new HarnessError(message, 'ARCFOLD_GOAL_CALL_INVALID'); };
9
+ export const resolveNativeGoalCall = (ctx, exec) => resolveNativeContextCall(ctx, exec, reject);
10
+
11
+ function assertSelection(args) {
12
+ const fields = ['id', 'revision', 'definitionVersion', 'contentHash', 'reason', 'sourceIds'];
13
+ if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== 1 ||
14
+ !Object.hasOwn(args, 'selection')) reject('Provide exactly one goal selection.');
15
+ const item = args.selection;
16
+ if (!item || typeof item !== 'object' || Array.isArray(item) || Object.keys(item).length !== fields.length ||
17
+ fields.some(field => !Object.hasOwn(item, field)))
18
+ reject('A goal selection requires exactly id, revision, definitionVersion, contentHash, reason and sourceIds.');
19
+ }
20
+
21
+ export function apply(ctx) {
22
+ ctx.on('tools/pre-execute', async (exec, next) => {
23
+ if (!belongsToSelectionBatch(exec, GOAL_TOOL)) return next();
24
+ try {
25
+ const current = await resolveNativeGoalCall(ctx, exec);
26
+ if (current.batch.length !== 1 || current.batch[0].name !== GOAL_TOOL)
27
+ return { kind: 'deny', reason: 'Request arcfold_link_goal alone. Read the returned goal context in the next model step before requesting any action.' };
28
+ } catch (error) { return { kind: 'deny', reason: error.message }; }
29
+ return next();
30
+ });
31
+ ctx.tools.register(defineTool({
32
+ name: GOAL_TOOL,
33
+ description: 'Relate this turn to one goal shown in the current Arcfold catalog, with your reason and its displayed sources. Call alone in the assistant tool batch. The receipt is followed by program goal context in the next step. A link is your interpretation, not user confirmation, new work authority, resumed tracking or proof that the long-term goal is complete.',
34
+ parameters: { selection: { type: 'object', required: true, additionalProperties: false, properties: {
35
+ id: { type: 'string', required: true }, revision: { type: 'integer', required: true },
36
+ definitionVersion: { type: 'integer', required: true }, contentHash: { type: 'string', required: true },
37
+ reason: { type: 'string', required: true }, sourceIds: { type: 'array', required: true, items: { type: 'string' } },
38
+ } } },
39
+ output: { schema: { type: 'json' }, render: (_args, receipt) => [{ type: 'text', text: JSON.stringify(receipt) }] },
40
+ async execute(args, exec) {
41
+ assertSelection(args);
42
+ const current = await resolveNativeGoalCall(ctx, exec);
43
+ if (current.batch.length !== 1 || current.batch[0].name !== GOAL_TOOL || typeof exec.deferContext !== 'function')
44
+ reject('The goal selection must be a standalone native tool call.');
45
+ const { receipt, bundle } = ctx.arcfoldGoals.link({ sessionId: current.sessionId, turn: current.turn,
46
+ startSeq: current.startSeq, callId: current.callId, contextSnapshotId: current.contextSnapshotId,
47
+ contextMessageId: current.contextMessageId, selection: args.selection,
48
+ maxCharacters: ctx.arcfoldBridge.maxContextCharacters });
49
+ exec.signal.throwIfAborted();
50
+ exec.deferContext(goalContextMessage(bundle));
51
+ exec.signal.throwIfAborted();
52
+ return receipt;
53
+ },
54
+ presentCall: () => ({ card: 'generic', title: 'Relate this work to a goal', kind: 'read' }),
55
+ }));
56
+ }