@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.
- package/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/cordis.patch.yml +138 -0
- package/package.json +70 -4
- package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
- package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
- package/presets/arcfold.patch.yml +110 -0
- package/runtime/README.md +19 -0
- package/runtime/bootstrap.json +15 -0
- package/runtime/desktop-python.tar.gz +0 -0
- package/runtime/host/package.json +11 -0
- package/runtime/host/pnpm-workspace.yaml +9 -0
- package/runtime/initial-policy.json +94 -0
- package/runtime/profile/pnpm-workspace.yaml +7 -0
- package/scripts/exact-package-patch.mjs +106 -0
- package/scripts/patch-deepseek-transport.mjs +14 -0
- package/src/agent.mjs +56 -0
- package/src/browse-scope.mjs +14 -0
- package/src/budget.mjs +369 -0
- package/src/business.mjs +334 -0
- package/src/context-material.mjs +119 -0
- package/src/deepseek-budget-plugin.mjs +58 -0
- package/src/deepseek-provider.mjs +57 -0
- package/src/deepseek-receipt.mjs +97 -0
- package/src/deepseek-transport.mjs +97 -0
- package/src/desktop/adapter.mjs +16 -0
- package/src/desktop/bootstrap.mjs +84 -0
- package/src/desktop/configuration.mjs +117 -0
- package/src/desktop/provider.mjs +46 -0
- package/src/desktop/python-runtime.mjs +105 -0
- package/src/desktop/setup-service.mjs +81 -0
- package/src/desktop/vendor/LICENSE.deepseek +21 -0
- package/src/desktop/vendor/NOTICE.md +5 -0
- package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
- package/src/evidence-ownership.mjs +51 -0
- package/src/evidence.mjs +35 -0
- package/src/goal-context.mjs +52 -0
- package/src/goal-execution-tool.mjs +74 -0
- package/src/goal-execution.mjs +333 -0
- package/src/goal-tool.mjs +56 -0
- package/src/goal-work-control.mjs +63 -0
- package/src/goal-work.mjs +257 -0
- package/src/goals.mjs +578 -0
- package/src/importer.mjs +298 -0
- package/src/index.mjs +53 -0
- package/src/learning-contracts.mjs +220 -0
- package/src/learning-defaults.mjs +98 -0
- package/src/learning-goals.mjs +165 -0
- package/src/learning-plugin.mjs +28 -0
- package/src/learning-runner.mjs +275 -0
- package/src/learning.mjs +534 -0
- package/src/legacy-export.mjs +332 -0
- package/src/legacy-import-source.mjs +356 -0
- package/src/metered-provider-host.mjs +101 -0
- package/src/metered-transport.mjs +435 -0
- package/src/method-context.mjs +57 -0
- package/src/method-lifecycle.mjs +34 -0
- package/src/method-tool.mjs +61 -0
- package/src/methods.mjs +425 -0
- package/src/model-selection.mjs +41 -0
- package/src/native-context-call.mjs +88 -0
- package/src/native-continuation.mjs +217 -0
- package/src/native-request-authority.mjs +90 -0
- package/src/openai-models.mjs +4 -0
- package/src/openai-receipt.mjs +98 -0
- package/src/openai-transport.mjs +167 -0
- package/src/openai-usage.mjs +194 -0
- package/src/opportunities.mjs +295 -0
- package/src/preparation-artifacts.mjs +213 -0
- package/src/preparation-plugin.mjs +290 -0
- package/src/runtime-command.mjs +264 -0
- package/src/runtime-config.mjs +417 -0
- package/src/runtime-guard.mjs +284 -0
- package/src/runtime-host.mjs +212 -0
- package/src/runtime-init.mjs +278 -0
- package/src/runtime-installation.mjs +518 -0
- package/src/runtime-lifecycle.mjs +227 -0
- package/src/runtime-model-settings.mjs +111 -0
- package/src/runtime-readiness.mjs +285 -0
- package/src/runtime-workspace.mjs +18 -0
- package/src/session-bridge.mjs +365 -0
- package/src/store.mjs +394 -0
- package/src/tree/LICENSE +21 -0
- package/src/tree/attribution.mjs +120 -0
- package/src/tree/clusters.mjs +61 -0
- package/src/tree/consolidation.mjs +103 -0
- package/src/tree/context.mjs +211 -0
- package/src/tree/contracts.mjs +142 -0
- package/src/tree/experience.mjs +140 -0
- package/src/tree/experiment-budget.mjs +84 -0
- package/src/tree/hooks.mjs +254 -0
- package/src/tree/host-context.mjs +107 -0
- package/src/tree/host-process.mjs +288 -0
- package/src/tree/host.mjs +37 -0
- package/src/tree/improvement.mjs +106 -0
- package/src/tree/items.mjs +294 -0
- package/src/tree/jobs.mjs +184 -0
- package/src/tree/learning.mjs +202 -0
- package/src/tree/metrics.mjs +203 -0
- package/src/tree/model-owner.mjs +254 -0
- package/src/tree/observations.mjs +73 -0
- package/src/tree/prompts.mjs +500 -0
- package/src/tree/reports.mjs +67 -0
- package/src/tree/repository.mjs +236 -0
- package/src/tree/runtime.mjs +745 -0
- package/src/tree/script-writer.mjs +149 -0
- package/src/tree/scripts.mjs +208 -0
- package/src/tree/signals.mjs +118 -0
- package/src/tree/snapshots.mjs +379 -0
- package/src/tree/token-counter.mjs +75 -0
- package/src/tree/token-counter.py +37 -0
- package/src/tree/tokenizer-resources/LICENSE +21 -0
- package/src/tree/tokenizer-resources/README.md +31 -0
- package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
- package/src/tree/tokenizer-resources/manifest.json +61 -0
- package/src/tree/tokenizer-resources/native.pyi +1042 -0
- package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
- package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
- package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
- package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
- package/src/tree/tools.mjs +99 -0
- package/src/tree/tree.mjs +202 -0
- package/src/tree/ui.mjs +285 -0
- package/src/tree/verification.mjs +143 -0
- package/src/understandings.mjs +309 -0
- package/src/web-budget-recovery.mjs +181 -0
package/src/methods.mjs
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Business selection and user decisions only. DSH owns tools, turns and requests.
|
|
2
|
+
import { DomainError, canonicalJson, identity } from './store.mjs';
|
|
3
|
+
import { hash, sourceText, dependenciesCurrent, evidenceProvenance } from './evidence.mjs';
|
|
4
|
+
import { invalidateDependents } from './learning-goals.mjs';
|
|
5
|
+
import { pauseMethod, methodPauseReason } from './method-lifecycle.mjs';
|
|
6
|
+
import { methodContextMessage } from './method-context.mjs';
|
|
7
|
+
import { browseScope, browseContext } from './browse-scope.mjs';
|
|
8
|
+
|
|
9
|
+
export { pauseMethod, methodPauseReason } from './method-lifecycle.mjs';
|
|
10
|
+
const fail = (code, message) => { throw new DomainError(code, message); };
|
|
11
|
+
const digest = value => hash(JSON.parse(canonicalJson(value)));
|
|
12
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
13
|
+
const nonnegative = value => Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
const notificationKinds = new Set(['method_paused', 'method_trial_allocated']);
|
|
15
|
+
const inScope = (row, sessionId) => !row.scope?.conversationId || row.scope.conversationId === sessionId;
|
|
16
|
+
const version = row => row.version ?? 1;
|
|
17
|
+
const contentOf = row => ({ id: row.id, version: version(row), title: row.title, guidance: row.guidance,
|
|
18
|
+
appliesTo: row.appliesTo, doesNotApplyTo: row.doesNotApplyTo, scope: row.scope ?? null,
|
|
19
|
+
sourceIds: row.sourceIds || [], dependencies: row.dependencies || [],
|
|
20
|
+
authority: 'work-guidance-only; no-new-tool-or-permission' });
|
|
21
|
+
export const methodContentHash = row => digest(contentOf(row));
|
|
22
|
+
|
|
23
|
+
function methodSourcesCurrent(store, method, cutoff = store.currentSequence()) {
|
|
24
|
+
return dependenciesCurrent(store, { ...method, status: 'trial' }, cutoff);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Old trialUses, when present, is a frozen import basis, never a live counter. */
|
|
28
|
+
export function usageForVersion(store, method, { cutoffSequence = store.currentSequence() } = {}) {
|
|
29
|
+
const legacyUses = method.trialUses ?? 0, maxTrialUses = method.maxTrialUses;
|
|
30
|
+
if (!nonnegative(legacyUses) || !positive(maxTrialUses) || !nonnegative(cutoffSequence))
|
|
31
|
+
fail('invalid_method_usage', '方法的历史试用次数、上限或统计截止不合法。');
|
|
32
|
+
const counted = store.listAll('method_adoptions', { where: { methodId: method.id } })
|
|
33
|
+
.filter(row => row.methodVersion === version(method) && row.countedTrial === true &&
|
|
34
|
+
row.createdSequence <= cutoffSequence).length;
|
|
35
|
+
const trialUses = legacyUses + counted;
|
|
36
|
+
if (!Number.isSafeInteger(trialUses)) fail('invalid_method_usage', '试用次数超出可表示范围。');
|
|
37
|
+
return { trialUses, maxTrialUses, remaining: Math.max(0, maxTrialUses - trialUses) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sourcesFor(store, method) {
|
|
41
|
+
const sources = new Map(), visited = new Set();
|
|
42
|
+
function visit(row) {
|
|
43
|
+
if (visited.has(row.id)) return;
|
|
44
|
+
visited.add(row.id);
|
|
45
|
+
for (const id of row.sourceIds || []) sources.set(id, store.get('events', id));
|
|
46
|
+
for (const ref of row.dependencies || []) {
|
|
47
|
+
const kind = ref.kind || ['events', 'understandings', 'methods'].find(k => store.get(k, ref.id));
|
|
48
|
+
const dependency = kind && store.get(kind, ref.id);
|
|
49
|
+
if (kind === 'events') sources.set(ref.id, dependency);
|
|
50
|
+
else if (dependency) visit(dependency);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
visit(method);
|
|
54
|
+
return [...sources.values()].filter(Boolean).map(source => ({ id: source.id, revision: source.revision,
|
|
55
|
+
role: source.role, content: sourceText(source), occurredAt: source.occurredAt ?? null,
|
|
56
|
+
provenance: evidenceProvenance(source.provenance) }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertNative({ sessionId, turn, startSeq }) {
|
|
60
|
+
identity(sessionId, 'Native session id');
|
|
61
|
+
if (!nonnegative(turn) || !nonnegative(startSeq)) fail('invalid_native_turn', '需要真实宿主轮次及开始序号。');
|
|
62
|
+
}
|
|
63
|
+
function pagination(offset, limit, maximum) {
|
|
64
|
+
if (!nonnegative(offset) || !positive(limit) || limit > maximum) fail('invalid_pagination', '分页参数不合法。');
|
|
65
|
+
}
|
|
66
|
+
const page = (rows, offset, limit) => ({ items: rows.slice(offset, offset + limit), total: rows.length,
|
|
67
|
+
nextOffset: offset + limit < rows.length ? offset + limit : null });
|
|
68
|
+
|
|
69
|
+
export class MethodService {
|
|
70
|
+
constructor({ business }) {
|
|
71
|
+
if (!business?.storage) fail('invalid_method_config', '需要业务存储。');
|
|
72
|
+
this.store = business.storage;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reconcile() {
|
|
76
|
+
return this.store.transaction(() => {
|
|
77
|
+
const paused = [];
|
|
78
|
+
for (const stale of this.store.listAll('methods')) {
|
|
79
|
+
const method = this.store.get('methods', stale.id);
|
|
80
|
+
if (!['trial', 'active'].includes(method.status)) continue;
|
|
81
|
+
let reason = !methodSourcesCurrent(this.store, method) ? 'source_changed' : null;
|
|
82
|
+
if (!reason && method.status === 'trial' && usageForVersion(this.store, method).remaining === 0) {
|
|
83
|
+
const open = this.store.listAll('method_adoptions', { where: { methodId: method.id } })
|
|
84
|
+
.some(row => row.methodVersion === version(method) && row.state !== 'settled');
|
|
85
|
+
if (!open) reason = 'trial_limit';
|
|
86
|
+
else this.ensureTrialNotice(method);
|
|
87
|
+
}
|
|
88
|
+
if (!reason) continue;
|
|
89
|
+
const saved = pauseMethod(this.store, method, { reason });
|
|
90
|
+
invalidateDependents(this.store, { ids: [method.id], pendingId: null, reason: 'method_' + reason });
|
|
91
|
+
paused.push(saved);
|
|
92
|
+
}
|
|
93
|
+
return paused;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
catalog({ sessionId, cutoffSequence = this.store.currentSequence() }) {
|
|
98
|
+
identity(sessionId, 'Native session id');
|
|
99
|
+
if (!nonnegative(cutoffSequence)) fail('invalid_cutoff', '需要有效的材料截止序号。');
|
|
100
|
+
this.reconcile();
|
|
101
|
+
return this.store.listAll('methods').filter(method => ['trial', 'active'].includes(method.status) &&
|
|
102
|
+
inScope(method, sessionId) && dependenciesCurrent(this.store, method, cutoffSequence)).flatMap(method => {
|
|
103
|
+
const usage = usageForVersion(this.store, method);
|
|
104
|
+
if (method.status === 'trial' && usage.remaining === 0) return [];
|
|
105
|
+
return [{ id: method.id, revision: method.revision, version: version(method), contentHash: methodContentHash(method),
|
|
106
|
+
title: method.title, appliesTo: method.appliesTo, doesNotApplyTo: method.doesNotApplyTo,
|
|
107
|
+
sourceIds: [...method.sourceIds], status: method.status, ...usage }];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Native identities and snapshot ids must be derived by the trusted adapter. */
|
|
112
|
+
reserve({ sessionId, turn, startSeq, callId, contextSnapshotId, contextMessageId, selections, maxCharacters = 24000 }) {
|
|
113
|
+
assertNative({ sessionId, turn, startSeq }); identity(callId, 'Native tool call id');
|
|
114
|
+
identity(contextSnapshotId, 'Context snapshot id'); identity(contextMessageId, 'Context message id');
|
|
115
|
+
if (!Array.isArray(selections) || selections.length < 1 || selections.length > 8 ||
|
|
116
|
+
new Set(selections.map(row => row?.id)).size !== selections.length ||
|
|
117
|
+
!positive(maxCharacters) || maxCharacters > 180000) fail('invalid_method_selection', '一次需选择一至八条不重复方法,并保留完整材料。');
|
|
118
|
+
this.reconcile();
|
|
119
|
+
return this.store.transaction(() => {
|
|
120
|
+
const snapshot = this.store.get('context_snapshots', contextSnapshotId);
|
|
121
|
+
if (this.store.get('host_bindings', sessionId)?.role !== 'main' || !snapshot ||
|
|
122
|
+
snapshot.sessionId !== sessionId || snapshot.turn !== turn || snapshot.contextMessageId !== contextMessageId ||
|
|
123
|
+
!Array.isArray(snapshot.material?.methods)) fail('method_context_mismatch', '选择没有对应的主会话已展示目录。');
|
|
124
|
+
const prepared = selections.map(selection => {
|
|
125
|
+
const shown = snapshot.material.methods.find(row => row.id === selection.id);
|
|
126
|
+
const method = this.store.get('methods', selection.id);
|
|
127
|
+
if (!shown || !method || shown.revision !== selection.revision || shown.contentHash !== selection.contentHash ||
|
|
128
|
+
method.revision !== selection.revision || methodContentHash(method) !== selection.contentHash)
|
|
129
|
+
fail('method_selection_changed', '方法未展示或版本已经改变,请重新读取目录。');
|
|
130
|
+
if (typeof selection.reason !== 'string' || !selection.reason.trim() || selection.reason.length > 2000 ||
|
|
131
|
+
!Array.isArray(selection.sourceIds) || !selection.sourceIds.length ||
|
|
132
|
+
new Set(selection.sourceIds).size !== selection.sourceIds.length ||
|
|
133
|
+
selection.sourceIds.some(id => !shown.sourceIds.includes(id)))
|
|
134
|
+
fail('invalid_method_selection', '选择须保留理由并引用所见目录中的来源。');
|
|
135
|
+
if (!['trial', 'active'].includes(method.status) || !inScope(method, sessionId) ||
|
|
136
|
+
!dependenciesCurrent(this.store, method, snapshot.material.cutoffSequence))
|
|
137
|
+
fail('method_unavailable', '方法或其原件已暂停、失效或越过展示截止。');
|
|
138
|
+
const contentHash = methodContentHash(method), methodVersion = version(method);
|
|
139
|
+
const id = 'method-adoption-' + digest([sessionId, startSeq, method.id, methodVersion, contentHash]);
|
|
140
|
+
const existing = this.store.get('method_adoptions', id);
|
|
141
|
+
if (existing && (existing.turn !== turn || existing.state === 'settled'))
|
|
142
|
+
fail('method_adoption_closed', '该采用所属轮次已经结束或身份不一致。');
|
|
143
|
+
if (!existing && method.status === 'trial' && usageForVersion(this.store, method).remaining === 0)
|
|
144
|
+
fail('method_trial_exhausted', '试用次数已用完,当前已领取工作可完成,新的采用需用户决定。');
|
|
145
|
+
const adoption = existing || { id, sessionId, turn, startSeq, methodId: method.id, methodVersion,
|
|
146
|
+
methodRevision: method.revision, contentHash, countedTrial: method.status === 'trial', state: 'reserved',
|
|
147
|
+
contextSnapshotId, contextMessageId, callIds: [], selection: { reason: selection.reason,
|
|
148
|
+
sourceIds: [...selection.sourceIds], claimType: 'inference' }, authority: 'selected-for-this-native-turn-only' };
|
|
149
|
+
return { method, adoption, existing };
|
|
150
|
+
});
|
|
151
|
+
const bundle = { version: 'arcfold-adopted-methods-v1', sessionId, turn, startSeq,
|
|
152
|
+
methods: prepared.map(({ method, adoption }) => this.bundleEntry(method, adoption)),
|
|
153
|
+
authority: 'program-provided guidance; not user instructions or new execution permission' };
|
|
154
|
+
// A later tool call may add methods, but cannot grow this turn's background
|
|
155
|
+
// without bound by repeatedly staying below the per-call capacity.
|
|
156
|
+
const current = this.currentBundle({ sessionId, turn, startSeq });
|
|
157
|
+
const combined = new Map([...current.methods, ...bundle.methods].map(entry => [entry.adoptionId, entry]));
|
|
158
|
+
if (methodContextMessage({ ...current, methods: [...combined.values()] }).content[0].text.length > maxCharacters)
|
|
159
|
+
fail('method_capacity', '本轮已选及新选方法的完整材料合计超过容量,本次未新增占用。');
|
|
160
|
+
const adoptions = prepared.map(({ adoption, existing }) => adoption.callIds.includes(callId) ? adoption
|
|
161
|
+
: this.store.save('method_adoptions', { ...adoption, callIds: [...adoption.callIds, callId] },
|
|
162
|
+
{ expectedRevision: existing?.revision ?? null }));
|
|
163
|
+
for (const { method } of prepared) this.ensureTrialNotice(method);
|
|
164
|
+
return { receipt: { adoptionIds: adoptions.map(row => row.id), methods: adoptions.map(row => ({
|
|
165
|
+
id: row.methodId, revision: row.methodRevision, version: row.methodVersion, contentHash: row.contentHash,
|
|
166
|
+
adoptionId: row.id })), authority: 'selection-receipt-only; not evidence of effective use' }, bundle };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
bundleEntry(method, adoption) {
|
|
171
|
+
return { adoptionId: adoption.id, method: contentOf(method), sources: sourcesFor(this.store, method),
|
|
172
|
+
selection: adoption.selection };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pendingAdoptions(method) {
|
|
176
|
+
const contentHash = methodContentHash(method);
|
|
177
|
+
return this.store.listAll('method_adoptions', { where: { methodId: method.id } })
|
|
178
|
+
.filter(row => row.methodVersion === version(method) && row.contentHash === contentHash && row.state !== 'settled')
|
|
179
|
+
.map(({ id, sessionId, turn, startSeq, callIds, state }) => ({ id, sessionId, turn, startSeq, callIds, state }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// This states a durable allocation fact, not whether a native writer is alive,
|
|
183
|
+
// whether a model received the method, or whether its use succeeded.
|
|
184
|
+
ensureTrialNotice(method) {
|
|
185
|
+
if (method.status !== 'trial' || usageForVersion(this.store, method).remaining !== 0) return null;
|
|
186
|
+
const contentHash = methodContentHash(method);
|
|
187
|
+
const id = 'method-trial-allocated-' + digest([method.id, version(method), contentHash]);
|
|
188
|
+
const existing = this.store.get('notifications', id);
|
|
189
|
+
if (existing) return existing;
|
|
190
|
+
const pendingAdoptions = this.pendingAdoptions(method);
|
|
191
|
+
if (!pendingAdoptions.length) return null;
|
|
192
|
+
return this.store.save('notifications', { id, kind: 'method_trial_allocated', status: 'unread',
|
|
193
|
+
methodId: method.id, methodRevision: method.revision, methodVersion: version(method), contentHash,
|
|
194
|
+
reason: 'trial_allocated', title: '协作方法试用名额已全部领取',
|
|
195
|
+
message: `“${method.title || '未命名方法'}”:登记提醒时,试用名额已全部领取;部分采用结果仍待核对;当时已领取工作可继续,新的采用等待你决定。`,
|
|
196
|
+
pendingAdoptions, pendingAdoptionCount: pendingAdoptions.length,
|
|
197
|
+
evidence: 'allocated-trial-usage; native-outcome-and-effectiveness-not-established' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Projection only. Host still checks successful tool receipts before sending. */
|
|
201
|
+
currentBundle({ sessionId, turn, startSeq }) {
|
|
202
|
+
assertNative({ sessionId, turn, startSeq });
|
|
203
|
+
return this.store.transaction(() => ({ version: 'arcfold-adopted-methods-v1', sessionId, turn, startSeq,
|
|
204
|
+
methods: this.store.listAll('method_adoptions', { where: { sessionId, startSeq } }).flatMap(adoption => {
|
|
205
|
+
if (adoption.turn !== turn) fail('method_adoption_conflict', '同一起点不能属于不同宿主轮次。');
|
|
206
|
+
if (adoption.state === 'settled') return [];
|
|
207
|
+
const method = this.store.get('methods', adoption.methodId);
|
|
208
|
+
if (!method || !['trial', 'active'].includes(method.status) || !inScope(method, sessionId) ||
|
|
209
|
+
version(method) !== adoption.methodVersion || methodContentHash(method) !== adoption.contentHash ||
|
|
210
|
+
!dependenciesCurrent(this.store, method, this.store.currentSequence())) return [];
|
|
211
|
+
return [this.bundleEntry(method, adoption)];
|
|
212
|
+
}), authority: 'program-provided guidance; not user instructions or new execution permission' }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
assertCurrent({ adoptionId, sessionId, turn, startSeq }) {
|
|
216
|
+
assertNative({ sessionId, turn, startSeq }); identity(adoptionId, 'Method adoption id');
|
|
217
|
+
this.reconcile();
|
|
218
|
+
const adoption = this.store.get('method_adoptions', adoptionId);
|
|
219
|
+
if (!adoption || adoption.sessionId !== sessionId || adoption.turn !== turn || adoption.startSeq !== startSeq ||
|
|
220
|
+
adoption.state === 'settled') fail('method_adoption_closed', '采用不属于当前有效宿主轮次。');
|
|
221
|
+
const method = this.store.get('methods', adoption.methodId);
|
|
222
|
+
if (!method || !['trial', 'active'].includes(method.status) || !inScope(method, sessionId) ||
|
|
223
|
+
version(method) !== adoption.methodVersion || methodContentHash(method) !== adoption.contentHash ||
|
|
224
|
+
!dependenciesCurrent(this.store, method, this.store.currentSequence()))
|
|
225
|
+
fail('method_unavailable', '所选方法的内容、来源或当前授权已变化。');
|
|
226
|
+
return { adoption, bundle: this.bundleEntry(method, adoption) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Validated input handed to DSH. A later provider guard may still reject it. */
|
|
230
|
+
recordInput({ adoptionIds, sessionId, turn, startSeq, step, requestIdentity, inputHash }) {
|
|
231
|
+
assertNative({ sessionId, turn, startSeq }); identity(requestIdentity, 'Native request identity');
|
|
232
|
+
if (!nonnegative(step) || !Array.isArray(adoptionIds) || !adoptionIds.length ||
|
|
233
|
+
new Set(adoptionIds).size !== adoptionIds.length || typeof inputHash !== 'string' || !/^[a-f0-9]{64}$/.test(inputHash))
|
|
234
|
+
fail('invalid_method_input', '需要原生模型步骤、不重复的采用编号和完整背景消息的摘要。');
|
|
235
|
+
return this.store.transaction(() => adoptionIds.map(adoptionId => {
|
|
236
|
+
this.assertCurrent({ adoptionId, sessionId, turn, startSeq });
|
|
237
|
+
const record = { id: 'method-input-' + digest([adoptionId, requestIdentity]), adoptionId,
|
|
238
|
+
sessionId, turn, startSeq, step, requestIdentity, inputHash,
|
|
239
|
+
evidence: 'validated-input-handed-to-host-provider; actual-send-requires-native-request-evidence' };
|
|
240
|
+
const old = this.store.get('method_inputs', record.id);
|
|
241
|
+
if (old) {
|
|
242
|
+
if (old.step !== step || old.sessionId !== sessionId || old.turn !== turn || old.startSeq !== startSeq || old.inputHash !== inputHash)
|
|
243
|
+
fail('method_input_conflict', '同一请求身份不能对应不同原生步骤或背景消息。');
|
|
244
|
+
return old;
|
|
245
|
+
}
|
|
246
|
+
return this.store.save('method_inputs', record);
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The bridge supplies an already persisted terminal native turn, never a job. */
|
|
251
|
+
settleTurn({ sessionId, turn, startSeq, outcome, deliveryId = null }) {
|
|
252
|
+
assertNative({ sessionId, turn, startSeq });
|
|
253
|
+
if (!['completed', 'cancelled', 'failed', 'interrupted', 'unknown'].includes(outcome)) fail('invalid_method_outcome', '需要宿主终态。');
|
|
254
|
+
return this.store.transaction(() => {
|
|
255
|
+
if (deliveryId) {
|
|
256
|
+
const delivery = this.store.get('deliveries', deliveryId);
|
|
257
|
+
if (!delivery || delivery.sessionId !== sessionId || delivery.turn !== turn || delivery.startSeq !== startSeq)
|
|
258
|
+
fail('method_delivery_mismatch', '正式交付不属于该原生轮次。');
|
|
259
|
+
}
|
|
260
|
+
const records = this.store.listAll('method_adoptions', { where: { sessionId, startSeq } }).map(row => {
|
|
261
|
+
if (row.turn !== turn) fail('method_adoption_conflict', '同一起点不能属于不同宿主轮次。');
|
|
262
|
+
if (row.state === 'settled') {
|
|
263
|
+
if (row.outcome !== outcome || row.deliveryId !== deliveryId) fail('method_outcome_conflict', '原生终态已经记录,不能改写。');
|
|
264
|
+
return row;
|
|
265
|
+
}
|
|
266
|
+
return this.store.save('method_adoptions', { ...row, state: 'settled', outcome, deliveryId,
|
|
267
|
+
effectiveness: 'not_assessed' }, { expectedRevision: row.revision });
|
|
268
|
+
});
|
|
269
|
+
this.reconcile();
|
|
270
|
+
return records;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
successors(method) {
|
|
275
|
+
return this.store.listAll('methods', { where: { previousId: method.id } })
|
|
276
|
+
.filter(row => version(row) > version(method))
|
|
277
|
+
.map(({ id, title, status, version }) => ({ id, title, status, version }));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
details(methodId) {
|
|
281
|
+
identity(methodId, 'Method id'); this.reconcile();
|
|
282
|
+
const method = this.store.get('methods', methodId);
|
|
283
|
+
if (!method) fail('method_not_found', '方法不存在。');
|
|
284
|
+
return this.detailView(method);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
detailView(method, { includeSources = true } = {}) {
|
|
288
|
+
const usage = usageForVersion(this.store, method), successors = this.successors(method);
|
|
289
|
+
const pendingAdoptions = this.pendingAdoptions(method);
|
|
290
|
+
const issue = !['trial', 'paused'].includes(method.status)
|
|
291
|
+
? { code: 'invalid_method_state', message: '只有试用或暂停的方法可以确认继续。' }
|
|
292
|
+
: successors.length ? { code: 'method_superseded', message: '已有后继版本,请查看新版本后决定。' }
|
|
293
|
+
: !methodSourcesCurrent(this.store, method) ? { code: 'method_source_invalid', message: '原件或依赖已经失效,需先修正内容。' } : null;
|
|
294
|
+
return { method, contentHash: methodContentHash(method), usage, successors,
|
|
295
|
+
pendingAdoptions, pendingAdoptionCount: pendingAdoptions.length,
|
|
296
|
+
reason: method.status === 'paused' ? methodPauseReason(method) : method.status === 'active'
|
|
297
|
+
? '你已明确确认在适用范围内继续采用,仍不代表效果已验证。'
|
|
298
|
+
: usage.remaining ? '正在有限试用;只有实际选入当前工作时才占用次数。' : '试用名额已用完,已领取的工作可继续完成;新的采用等待你决定。',
|
|
299
|
+
needsDecision: !successors.length && method.pauseReason !== 'user_request' &&
|
|
300
|
+
(method.status === 'paused' || method.status === 'trial' && usage.remaining === 0),
|
|
301
|
+
activation: { allowed: !issue, ...(issue || {}) }, sources: includeSources ? sourcesFor(this.store, method) : [] };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
unavailableView(methodId, methodVersion) {
|
|
305
|
+
return { method: { id: methodId, title: '方法记录暂不可用', status: 'unavailable', version: methodVersion },
|
|
306
|
+
reason: '未找到这条方法的当前记录,历史提醒仍保留。', needsDecision: false, usage: null,
|
|
307
|
+
activation: { allowed: false, code: 'method_not_found', message: '当前方法记录不可用,不能作出采用决定。' },
|
|
308
|
+
sources: [], successors: [], pendingAdoptions: [], pendingAdoptionCount: 0 };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Group the complete collections before paging. Two independently paged
|
|
312
|
+
// lists cannot establish one current card per method or complete history.
|
|
313
|
+
listCards({ sessionId, viewScope, offset = 0, limit = 20 } = {}) {
|
|
314
|
+
pagination(offset, limit, 100);
|
|
315
|
+
const scope = browseScope(viewScope, 'all');
|
|
316
|
+
if (viewScope !== undefined) identity(sessionId, 'Browsing session');
|
|
317
|
+
return this.store.transaction(() => {
|
|
318
|
+
this.reconcile();
|
|
319
|
+
const histories = new Map();
|
|
320
|
+
for (const notice of this.store.listAll('notifications').filter(row => notificationKinds.has(row.kind))) {
|
|
321
|
+
if (!histories.has(notice.methodId)) histories.set(notice.methodId, []);
|
|
322
|
+
histories.get(notice.methodId).push(notice);
|
|
323
|
+
}
|
|
324
|
+
const methods = new Map(this.store.listAll('methods').map(row => [row.id, row]));
|
|
325
|
+
const rows = [...new Set([...methods.keys(), ...histories.keys()])].flatMap(id => {
|
|
326
|
+
const method = methods.get(id), history = histories.get(id) || [];
|
|
327
|
+
if (scope === 'current' && (!method || !inScope(method, sessionId))) return [];
|
|
328
|
+
const detail = method ? this.detailView(method, { includeSources: false })
|
|
329
|
+
: this.unavailableView(id, history.at(-1)?.methodVersion);
|
|
330
|
+
// Keep the original management endpoint compatible. The explicit
|
|
331
|
+
// browse view includes active/trial methods without notices as well.
|
|
332
|
+
if (viewScope === undefined && !detail.needsDecision && !history.length) return [];
|
|
333
|
+
return [{ method: { id, title: detail.method.title, version: detail.method.version ?? 1,
|
|
334
|
+
revision: detail.method.revision, status: detail.method.status },
|
|
335
|
+
reason: detail.reason, needsDecision: detail.needsDecision,
|
|
336
|
+
...(method ? { browse: browseContext(method, sessionId) } : {}),
|
|
337
|
+
notificationCount: history.length, unreadCount: history.filter(row => row.status === 'unread').length,
|
|
338
|
+
order: history.reduce((latest, row) => Math.max(latest, row.createdSequence ?? 0), method?.updatedSequence ?? 0) }];
|
|
339
|
+
}).sort((a, b) => Number(b.needsDecision) - Number(a.needsDecision) || b.order - a.order || a.method.id.localeCompare(b.method.id));
|
|
340
|
+
return { ...page(rows.map(({ order, ...row }) => row), offset, limit),
|
|
341
|
+
decisionCount: rows.filter(row => row.needsDecision).length,
|
|
342
|
+
unreadCount: rows.reduce((count, row) => count + row.unreadCount, 0) };
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
cardDetails(methodId, { sessionId, viewScope, offset = 0, limit = 20 } = {}) {
|
|
347
|
+
identity(methodId, 'Method id'); pagination(offset, limit, 100);
|
|
348
|
+
const scope = browseScope(viewScope, 'all');
|
|
349
|
+
if (viewScope !== undefined) identity(sessionId, 'Browsing session');
|
|
350
|
+
return this.store.transaction(() => {
|
|
351
|
+
const notifications = this.notifications({ methodId, offset, limit });
|
|
352
|
+
const method = this.store.get('methods', methodId);
|
|
353
|
+
if (!method && !notifications.total) fail('method_not_found', '方法不存在。');
|
|
354
|
+
if (scope === 'current' && (!method || !inScope(method, sessionId)))
|
|
355
|
+
fail('method_not_found', '此方法不在当前聊天筛选范围。');
|
|
356
|
+
const detail = method ? this.detailView(method)
|
|
357
|
+
: this.unavailableView(methodId, notifications.items[0]?.methodVersion);
|
|
358
|
+
return { ...detail, ...(method ? { browse: browseContext(method, sessionId) } : {}),
|
|
359
|
+
notifications: { ...notifications, offset } };
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
listDecisions({ offset = 0, limit = 20 } = {}) {
|
|
364
|
+
pagination(offset, limit, 100);
|
|
365
|
+
return this.store.transaction(() => {
|
|
366
|
+
this.reconcile();
|
|
367
|
+
const rows = this.store.listAll('methods').map(row => this.detailView(row)).filter(row => row.needsDecision).reverse();
|
|
368
|
+
return page(rows, offset, limit);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
notifications({ offset = 0, limit = 100, unread = false, methodId } = {}) {
|
|
373
|
+
pagination(offset, limit, 200);
|
|
374
|
+
if (typeof unread !== 'boolean') fail('invalid_pagination', '通知筛选参数不合法。');
|
|
375
|
+
if (methodId !== undefined) identity(methodId, 'Method id');
|
|
376
|
+
this.reconcile();
|
|
377
|
+
const legacySuffix = '学习提出暂停:import_batch_withdrawn';
|
|
378
|
+
const rows = this.store.listAll('notifications').filter(row => notificationKinds.has(row.kind) &&
|
|
379
|
+
(methodId === undefined || row.methodId === methodId)).reverse()
|
|
380
|
+
// Preserve the notice's own title prefix and persisted wording. Correct
|
|
381
|
+
// only the exact old suffix; never reinterpret it from today's method.
|
|
382
|
+
.map(row => row.kind === 'method_paused' && row.reason === 'import_batch_withdrawn' &&
|
|
383
|
+
typeof row.message === 'string' && row.message.endsWith(legacySuffix)
|
|
384
|
+
? { ...row, displayMessage: row.message.slice(0, -legacySuffix.length) +
|
|
385
|
+
methodPauseReason({ pauseReason: row.reason }) } : row);
|
|
386
|
+
return { ...page(unread ? rows.filter(row => row.status === 'unread') : rows, offset, limit),
|
|
387
|
+
unreadCount: rows.filter(row => row.status === 'unread').length };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
acknowledgeNotification(notificationId) {
|
|
391
|
+
return this.store.transaction(() => {
|
|
392
|
+
const row = this.store.get('notifications', notificationId);
|
|
393
|
+
if (!row || !notificationKinds.has(row.kind)) fail('notification_not_found', '方法通知不存在。');
|
|
394
|
+
return row.status === 'read' ? row : this.store.save('notifications', { ...row, status: 'read' }, { expectedRevision: row.revision });
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Trusted browser handler only. Never register this as a model-facing tool. */
|
|
399
|
+
decide({ methodId, expectedRevision, contentHash, action, requestId, userIdentity }) {
|
|
400
|
+
identity(methodId, 'Method id'); identity(requestId, 'User decision request id'); identity(userIdentity, 'Trusted user identity');
|
|
401
|
+
if (!positive(expectedRevision) || typeof contentHash !== 'string' || !['continue', 'pause'].includes(action))
|
|
402
|
+
fail('invalid_method_decision', '用户决定须绑定所见方法版本和具体操作。');
|
|
403
|
+
const request = { methodId, expectedRevision, contentHash, action, requestId, userIdentity };
|
|
404
|
+
this.reconcile();
|
|
405
|
+
return this.store.transaction(() => {
|
|
406
|
+
const id = 'method-decision-' + digest([userIdentity, requestId]), existing = this.store.get('method_decisions', id);
|
|
407
|
+
if (existing) {
|
|
408
|
+
if (canonicalJson(existing.request) !== canonicalJson(request)) fail('decision_identity_conflict', '同一决定请求不能更换内容。');
|
|
409
|
+
return { method: existing.method, decision: existing };
|
|
410
|
+
}
|
|
411
|
+
const detail = this.details(methodId), before = detail.method;
|
|
412
|
+
if (before.revision !== expectedRevision || detail.contentHash !== contentHash) fail('revision_conflict', '方法已变化,请重新查看全文和停止理由。');
|
|
413
|
+
if (action === 'continue' && !detail.activation.allowed) fail(detail.activation.code, detail.activation.message);
|
|
414
|
+
const method = action === 'pause' ? pauseMethod(this.store, before, { reason: 'user_request' })
|
|
415
|
+
: this.store.save('methods', { ...before, status: 'active', validated: false, pauseReason: null,
|
|
416
|
+
withdrawalReason: null, activationReason: 'explicit_user_decision', activationDecisionId: id },
|
|
417
|
+
{ expectedRevision: before.revision });
|
|
418
|
+
if (action === 'pause') invalidateDependents(this.store, { ids: [methodId], pendingId: null, reason: 'method_user_request' });
|
|
419
|
+
const decision = this.store.save('method_decisions', { id, request, actor: 'user',
|
|
420
|
+
provenance: { kind: 'trusted_user_method_decision', userIdentity }, before, method,
|
|
421
|
+
authority: 'current-method-guidance-only; no-additional-tool-or-work-permission' });
|
|
422
|
+
return { method, decision };
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Native main selection has one owner: its session projection and default service.
|
|
2
|
+
const fields = ['provider', 'model', 'reasoningEffort'];
|
|
3
|
+
const same = (a, b) => a && b && fields.every(key => a[key] === b[key]);
|
|
4
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
5
|
+
|
|
6
|
+
/** The runtime supplies verified native services and its current route validator.
|
|
7
|
+
* No listener is installed here: startup must independently verify the actual
|
|
8
|
+
* consuming controller, unique hook owner and service identities before enabling it.
|
|
9
|
+
*/
|
|
10
|
+
export function createModelSelectionHandler({ sessions, defaults, assertSelection }) {
|
|
11
|
+
if (typeof assertSelection !== 'function') throw new TypeError('A current route validator is required');
|
|
12
|
+
return async ({ agent, selected }) => {
|
|
13
|
+
if (!agent || sessions.get(agent.id) !== agent.session || !Object.isFrozen(selected))
|
|
14
|
+
fail('invalid_model_selection_owner', '无法确认当前会话的模型设置归属。');
|
|
15
|
+
let entered = false;
|
|
16
|
+
try {
|
|
17
|
+
return await agent.runMaintenance(async signal => {
|
|
18
|
+
entered = true;
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
await assertSelection(agent, selected);
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
await defaults.saveSelection(selected);
|
|
23
|
+
// Saving the default and appending a session event are separate native
|
|
24
|
+
// writes. A later failure is reported; neither is falsely rolled back.
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
await assertSelection(agent, selected);
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
agent.model.select(selected);
|
|
29
|
+
if (await sessions.flush(agent.session) !== true)
|
|
30
|
+
fail('model_selection_not_durable', '默认设置可能已保存,但当前会话尚未确认写入;请检查当前设置后重试。');
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
if (!same(agent.model.current(), selected) || !same(defaults.currentSelection(), selected))
|
|
33
|
+
fail('model_selection_readback_changed', '设置保存期间发生了变化,请刷新查看实际选择。');
|
|
34
|
+
return { selected: { ...selected } };
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!entered) fail('model_selection_busy', '当前回答仍在进行,请在本次回答结束后保存模型设置。');
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Shared native identity/catalog checks; the host remains the execution owner.
|
|
2
|
+
import { canonicalJson } from './store.mjs';
|
|
3
|
+
import { contextMessage } from './context-material.mjs';
|
|
4
|
+
|
|
5
|
+
const nonnegative = value => Number.isSafeInteger(value) && value >= 0;
|
|
6
|
+
|
|
7
|
+
function liveCaller(ctx, exec, reject) {
|
|
8
|
+
if (!exec.signal || typeof exec.signal.throwIfAborted !== 'function') reject('A native cancellation signal is required.');
|
|
9
|
+
exec.signal.throwIfAborted();
|
|
10
|
+
const agent = exec.agent;
|
|
11
|
+
if (!agent || ctx.agents.get(agent.id) !== agent || agent.status !== 'running' ||
|
|
12
|
+
ctx.agents.currentInitiator() !== agent || !ctx.arcfoldBridge.isBound(agent.session?.id))
|
|
13
|
+
reject('Context selection requires the exact live Arcfold main agent in its current driver.');
|
|
14
|
+
if (exec.parent !== undefined || exec.rootCallId !== exec.callId || typeof exec.callId !== 'string' || !exec.callId)
|
|
15
|
+
reject('Context selection requires a direct native call, not a nested or PTC call.');
|
|
16
|
+
const boundary = ctx.agents.turnBoundaryOf(agent.session);
|
|
17
|
+
if (!nonnegative(boundary.openTurnStartSeq) || boundary.lastStepBoundary?.kind !== 'start')
|
|
18
|
+
reject('Context selection requires an open native model step.');
|
|
19
|
+
return { agent, boundary };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function callEvidence(events, exec, reject) {
|
|
23
|
+
const calls = events.filter(event => event.type === 'tool/call' && event.data.callId === exec.callId);
|
|
24
|
+
if (calls.length !== 1) reject('This call has no unique native tool/call record.');
|
|
25
|
+
const call = calls[0];
|
|
26
|
+
const assistant = events.findLast(event => event.seq < call.seq && event.type === 'assistant/message');
|
|
27
|
+
if (!assistant || assistant.data.message?.role !== 'assistant' || assistant.data.message?.source?.kind !== 'model' ||
|
|
28
|
+
assistant.data.interrupted || assistant.data.turn !== call.data.turn || assistant.data.step !== call.data.step)
|
|
29
|
+
reject('This call does not belong to the completed assistant message in its native step.');
|
|
30
|
+
const batch = assistant.data.message.content.filter(block => block.type === 'tool-call');
|
|
31
|
+
const matching = batch.filter(block => block.id === exec.callId);
|
|
32
|
+
if (matching.length !== 1 || new Set(batch.map(block => block.id)).size !== batch.length ||
|
|
33
|
+
call.data.name !== exec.name || matching[0].name !== exec.name || matching[0].arguments !== call.data.arguments)
|
|
34
|
+
reject('The native tool call differs from the assistant tool-call list.');
|
|
35
|
+
let argumentsValue;
|
|
36
|
+
try { argumentsValue = JSON.parse(call.data.arguments); } catch { reject('The native tool arguments are not JSON.'); }
|
|
37
|
+
if (canonicalJson(argumentsValue) !== canonicalJson(exec.arguments)) reject('The execution arguments differ from the native call.');
|
|
38
|
+
if (events.some(event => event.seq > call.seq && event.type === 'tool/result' && event.data.message?.toolCallId === exec.callId))
|
|
39
|
+
reject('This native call already has a result.');
|
|
40
|
+
return { call, assistant, batch };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Native driver + flushed call + current logged surface + saved material, not a second execution identity. */
|
|
44
|
+
export async function resolveNativeContextCall(ctx, exec, reject) {
|
|
45
|
+
const before = liveCaller(ctx, exec, reject), session = before.agent.session;
|
|
46
|
+
const events = await ctx.arcfoldBridge.durablePrefix(session);
|
|
47
|
+
const after = liveCaller(ctx, exec, reject);
|
|
48
|
+
if (after.agent !== before.agent || after.boundary.openTurnStartSeq !== before.boundary.openTurnStartSeq ||
|
|
49
|
+
after.boundary.lastStepStartSeq !== before.boundary.lastStepStartSeq)
|
|
50
|
+
reject('The native driver changed while its call was being flushed.');
|
|
51
|
+
const startSeq = before.boundary.openTurnStartSeq;
|
|
52
|
+
const start = events.find(event => event.seq === startSeq && event.type === 'turn/start');
|
|
53
|
+
const stepStart = events.find(event => event.seq === before.boundary.lastStepStartSeq && event.type === 'step/start');
|
|
54
|
+
const evidence = callEvidence(events, exec, reject);
|
|
55
|
+
if (!start || !stepStart || !nonnegative(start.data.turn) || !nonnegative(stepStart.data.step) ||
|
|
56
|
+
stepStart.seq <= startSeq || stepStart.data.turn !== start.data.turn ||
|
|
57
|
+
evidence.assistant.seq <= stepStart.seq || evidence.call.data.turn !== start.data.turn ||
|
|
58
|
+
evidence.call.data.step !== stepStart.data.step || events.some(event => event.seq > startSeq && event.type === 'turn/end') ||
|
|
59
|
+
events.some(event => event.seq > stepStart.seq && event.type === 'step/end'))
|
|
60
|
+
reject('The call is outside the current unfinished native turn and step.');
|
|
61
|
+
|
|
62
|
+
const messages = session.deriveMessages().filter(message => message.source?.kind === 'arcfold/context');
|
|
63
|
+
if (messages.length !== 1) reject('The native surface has no unique current Arcfold catalog.');
|
|
64
|
+
const message = messages[0];
|
|
65
|
+
const originals = events.filter(event => event.type === 'user/message' && event.data.id === message.id &&
|
|
66
|
+
event.data.source?.kind === 'arcfold/context');
|
|
67
|
+
if (originals.length !== 1 || originals[0].seq <= startSeq || originals[0].seq >= evidence.assistant.seq ||
|
|
68
|
+
canonicalJson(originals[0].data) !== canonicalJson(message))
|
|
69
|
+
reject('The visible catalog has no matching durable original before this assistant response.');
|
|
70
|
+
const snapshots = ctx.arcfoldBridge.storage.listAll('context_snapshots', {
|
|
71
|
+
where: { sessionId: session.id, turn: start.data.turn, contextMessageId: message.id },
|
|
72
|
+
});
|
|
73
|
+
if (snapshots.length !== 1 || canonicalJson(contextMessage(snapshots[0].material)) !== canonicalJson(message))
|
|
74
|
+
reject('The native catalog is not the frozen material saved for this turn.');
|
|
75
|
+
return { ...evidence, agent: before.agent, events, sessionId: session.id, turn: start.data.turn,
|
|
76
|
+
step: stepStart.data.step, startSeq, callId: exec.callId,
|
|
77
|
+
contextSnapshotId: snapshots[0].id, contextMessageId: message.id };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function belongsToSelectionBatch(exec, toolName) {
|
|
81
|
+
if (exec.name === toolName) return true;
|
|
82
|
+
const events = exec.agent?.session?.snapshotEvents?.();
|
|
83
|
+
if (!events) return false;
|
|
84
|
+
const call = events.findLast(event => event.type === 'tool/call' && event.data.callId === exec.callId);
|
|
85
|
+
const assistant = call && events.findLast(event => event.seq < call.seq && event.type === 'assistant/message' &&
|
|
86
|
+
event.data.turn === call.data.turn && event.data.step === call.data.step);
|
|
87
|
+
return assistant?.data.message.content.some(block => block.type === 'tool-call' && block.name === toolName) === true;
|
|
88
|
+
}
|