@yeaft/webchat-agent 1.0.629 → 1.0.631
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +69 -69
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/llm/adapter.js +15 -0
- package/yeaft/llm/anthropic.js +49 -16
- package/yeaft/llm/openai-responses.js +179 -61
- package/yeaft/person/capabilities.js +61 -5
- package/yeaft/person/contracts.js +7 -13
- package/yeaft/person/execution-evidence.js +14 -0
- package/yeaft/person/execution-protocol.js +288 -0
- package/yeaft/person/local-memory-worker.js +23 -13
- package/yeaft/person/native-tools.js +17 -10
- package/yeaft/person/provider.js +65 -18
- package/yeaft/person/request-budget.js +26 -0
- package/yeaft/person/runtime.js +155 -142
- package/yeaft/person/service.js +17 -6
- package/yeaft/person/sqlite-repository.js +3 -0
- package/yeaft/person/sqlite-store.js +113 -38
- package/yeaft/person/tool-arguments.js +76 -0
- package/yeaft/person/turn-diagnostics.js +23 -7
- package/yeaft/tools/bash.js +20 -17
- package/yeaft/tools/file-edit.js +4 -1
package/yeaft/person/provider.js
CHANGED
|
@@ -4,9 +4,13 @@ import { applyAnthropicThinking } from '../llm/anthropic.js';
|
|
|
4
4
|
import { inferProtocolFromModelId, normalizeModelEntry } from '../llm/router.js';
|
|
5
5
|
import { normalizeKnownProviderForRuntime } from '../llm/known-providers.js';
|
|
6
6
|
import { normalizeEffort, resolveContextWindow, resolveMaxOutputTokens } from '../models.js';
|
|
7
|
+
import { bindProviderState, providerStateBytes, MAX_PROVIDER_STATE_BYTES } from '../llm/provider-state.js';
|
|
7
8
|
import { utf8PrefixWithinBytes } from '../utf8.js';
|
|
8
9
|
import { bytes, digest, fail, LIMITS, PersonError } from './contracts.js';
|
|
9
10
|
import { addUsage } from './turn-diagnostics.js';
|
|
11
|
+
import { defaultPersonMinimumRequestBytes, PERSON_ENVELOPE_TOKENS_RESERVED } from './request-budget.js';
|
|
12
|
+
|
|
13
|
+
export const NATIVE_CONTEXT_UNAVAILABLE_REASON = 'The model context window cannot fit the native tool schemas, execution instructions and final-cognition reserve. Choose a model with a larger context window.';
|
|
10
14
|
|
|
11
15
|
export const MODEL_LIMITS = Object.freeze({ candidates: 8, available: 100 });
|
|
12
16
|
export function validateModelCandidates(refs) {
|
|
@@ -27,14 +31,26 @@ export function resolveAgentDefaultModel(config) {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/** Shared validation for settings, status and runtime; never broaden an explicit subset. */
|
|
30
|
-
export function selectPersonModels({ availableModels, modelCandidates = [], defaultModel = null }) {
|
|
34
|
+
export function selectPersonModels({ availableModels, unavailableModels = [], modelCandidates = [], defaultModel = null }) {
|
|
31
35
|
validateModelCandidates(modelCandidates);
|
|
32
36
|
validateDefaultModel(defaultModel);
|
|
33
|
-
if (modelCandidates.some(ref => !availableModels.some(model => model.id === ref)))
|
|
37
|
+
if (modelCandidates.some(ref => !availableModels.some(model => model.id === ref))) {
|
|
38
|
+
const error = new PersonError('MODEL_SELECTION');
|
|
39
|
+
if (modelCandidates.some(ref => unavailableModels.some(model => model.id === ref))) error.message = NATIVE_CONTEXT_UNAVAILABLE_REASON;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
34
42
|
const catalog = modelCandidates.length ? availableModels.filter(model => modelCandidates.includes(model.id))
|
|
35
43
|
: availableModels.slice(0, MODEL_LIMITS.candidates);
|
|
36
|
-
if (defaultModel !== null && !catalog.some(model => model.id === defaultModel))
|
|
37
|
-
|
|
44
|
+
if (defaultModel !== null && !catalog.some(model => model.id === defaultModel)) {
|
|
45
|
+
const error = new PersonError('MODEL_SELECTION');
|
|
46
|
+
if (unavailableModels.some(model => model.id === defaultModel)) error.message = NATIVE_CONTEXT_UNAVAILABLE_REASON;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (!catalog.length) {
|
|
50
|
+
const error = new PersonError('MODEL_UNAVAILABLE');
|
|
51
|
+
if (unavailableModels.length) error.message = NATIVE_CONTEXT_UNAVAILABLE_REASON;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
38
54
|
return { catalog, defaultSelection: { model: defaultModel ?? catalog[0].id, effort: null } };
|
|
39
55
|
}
|
|
40
56
|
|
|
@@ -62,16 +78,18 @@ function imageInputBudget(modelId, protocol) {
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/** Only configured native API models; no Session initialization or implicit credential fallback. */
|
|
65
|
-
export async function createPersonProvider({ yeaftDir, config: suppliedConfig, adapter: suppliedAdapter, allowedModels, effortEnabled = process.env.YEAFT_THINKING_V1 === '1', modelCandidates = [], defaultModel = null }) {
|
|
81
|
+
export async function createPersonProvider({ yeaftDir, config: suppliedConfig, adapter: suppliedAdapter, allowedModels, effortEnabled = process.env.YEAFT_THINKING_V1 === '1', modelCandidates = [], defaultModel = null, minimumRequestBytes }) {
|
|
66
82
|
const config = structuredClone(suppliedConfig || loadConfig({ dir: yeaftDir }));
|
|
67
83
|
if (!config.providers?.length && !suppliedAdapter) fail('MODEL_UNAVAILABLE');
|
|
84
|
+
minimumRequestBytes ??= await defaultPersonMinimumRequestBytes(config);
|
|
85
|
+
if (!Number.isSafeInteger(minimumRequestBytes) || minimumRequestBytes < 1) fail('INVALID_REQUEST');
|
|
68
86
|
const models = config.availableModels || [];
|
|
69
87
|
const agentDefaultModel = resolveAgentDefaultModel(config);
|
|
70
88
|
validateModelCandidates(modelCandidates);
|
|
71
89
|
validateDefaultModel(defaultModel);
|
|
72
90
|
const available = models.filter(m => !allowedModels || allowedModels.includes(m.ref || m.id));
|
|
73
91
|
const routingProviders = (config.providers || []).map(normalizeKnownProviderForRuntime);
|
|
74
|
-
const
|
|
92
|
+
const resolvedModels = available.map(m => {
|
|
75
93
|
// Use the same model/config resolution as the native Engine, without a Person-only token ceiling.
|
|
76
94
|
const maxOutput = Math.floor(resolveMaxOutputTokens(m.id, { ...config, modelInfo: m }));
|
|
77
95
|
const effortContext = { ...m, thinkingProtocol: m.effortProtocol || m.thinkingProtocol };
|
|
@@ -96,16 +114,21 @@ export async function createPersonProvider({ yeaftDir, config: suppliedConfig, a
|
|
|
96
114
|
const imageBudget = !routedProvider || explicitImages === false ? null : imageInputBudget(m.id, protocol);
|
|
97
115
|
return { id: m.ref || m.id, efforts, maxOutput, usageCacheIncluded: protocol === 'anthropic' ? false : undefined, supportsImages: imageBudget !== null, imageBudget,
|
|
98
116
|
contextWindow: Math.floor(resolveContextWindow(m.id, { ...config, modelInfo: m })) };
|
|
99
|
-
}).filter(m => typeof m.id === 'string' && m.id.length <= 256 && m.contextWindow
|
|
117
|
+
}).filter(m => typeof m.id === 'string' && m.id.length <= 256 && Number.isSafeInteger(m.contextWindow) &&
|
|
118
|
+
Number.isSafeInteger(m.maxOutput) && m.contextWindow > m.maxOutput + PERSON_ENVELOPE_TOKENS_RESERVED && m.maxOutput >= 256);
|
|
119
|
+
const fitsNativeRequest = model => model.contextWindow - model.maxOutput - PERSON_ENVELOPE_TOKENS_RESERVED > minimumRequestBytes;
|
|
120
|
+
const unavailableModels = resolvedModels.filter(model => !fitsNativeRequest(model))
|
|
121
|
+
.slice(0, MODEL_LIMITS.available).map(model => ({ id: model.id, reason: NATIVE_CONTEXT_UNAVAILABLE_REASON }));
|
|
122
|
+
const safeModels = resolvedModels.filter(fitsNativeRequest);
|
|
100
123
|
safeModels.sort((a, b) => Number(b.id === agentDefaultModel) - Number(a.id === agentDefaultModel));
|
|
101
124
|
const seen = new Set();
|
|
102
125
|
const uniqueModels = safeModels.filter(model => !seen.has(model.id) && seen.add(model.id));
|
|
103
126
|
const availableModels = uniqueModels.slice(0, MODEL_LIMITS.available);
|
|
104
127
|
// A UI catalog must not be the bounded episode choice set: model 9+ remains selectable.
|
|
105
128
|
// Explicit lists are validated, never silently truncated or broadened on stale config.
|
|
106
|
-
const { catalog, defaultSelection } = selectPersonModels({ availableModels, modelCandidates, defaultModel });
|
|
129
|
+
const { catalog, defaultSelection } = selectPersonModels({ availableModels, unavailableModels, modelCandidates, defaultModel });
|
|
107
130
|
const adapter = suppliedAdapter || await createLLMAdapter(config);
|
|
108
|
-
return { adapter, catalog, availableModels, availableModelsTruncated: uniqueModels.length > MODEL_LIMITS.available,
|
|
131
|
+
return { adapter, catalog, availableModels, unavailableModels, availableModelsTruncated: uniqueModels.length > MODEL_LIMITS.available,
|
|
109
132
|
catalogRevision: digest(catalog), defaultSelection, agentDefaultModel, effortEnabled };
|
|
110
133
|
}
|
|
111
134
|
|
|
@@ -123,14 +146,17 @@ export function abortable(promise, signal) {
|
|
|
123
146
|
});
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
/** Drain a complete native response before any tool dispatch. Archive public
|
|
150
|
+
* text/calls/usage only; fenced opaque continuation remains episode-local RAM. */
|
|
151
|
+
export async function collectOutput(adapter, params, onEffort = () => {}) {
|
|
152
|
+
const iterator = adapter.stream({ ...params, tools: params.tools ?? [], onEffortDecision: decision => onEffort({
|
|
129
153
|
effective: typeof decision.effective === 'string' ? decision.effective : null,
|
|
130
154
|
wireMode: typeof decision.wireMode === 'string' ? decision.wireMode : null,
|
|
131
155
|
thinkingEnabled: decision.thinkingEnabled === true,
|
|
132
156
|
}) })[Symbol.asyncIterator]();
|
|
133
157
|
let output = '', size = 0, stopReason = null, usage = null;
|
|
158
|
+
const toolCalls = [], toolIds = new Set();
|
|
159
|
+
let providerState = null;
|
|
134
160
|
try {
|
|
135
161
|
while (true) {
|
|
136
162
|
const item = await abortable(iterator.next(), params.signal);
|
|
@@ -145,22 +171,43 @@ export async function collectOutput(adapter, params, onEffort) {
|
|
|
145
171
|
fail('OUTPUT_LIMIT');
|
|
146
172
|
}
|
|
147
173
|
output += event.text;
|
|
174
|
+
} else if (event.type === 'provider_state') {
|
|
175
|
+
if (providerStateBytes(event.providerState) > MAX_PROVIDER_STATE_BYTES) fail('CONTEXT_LIMIT');
|
|
176
|
+
providerState = event.providerState;
|
|
148
177
|
} else if (event.type === 'stop') stopReason = event.stopReason;
|
|
149
|
-
else if (event.type === 'error') fail('PROVIDER_FAILED');
|
|
150
|
-
else if (event.type === 'tool_call')
|
|
178
|
+
else if (event.type === 'error') { if (event.error?.toolInputRejected === true) fail('INVALID_TOOL_INPUT'); fail('PROVIDER_FAILED'); }
|
|
179
|
+
else if (event.type === 'tool_call') {
|
|
180
|
+
if (!params.tools?.length || typeof event.id !== 'string' || !event.id || event.id.length > 256 || toolIds.has(event.id) || typeof event.name !== 'string' || event.name.length > 128) fail('INVALID_PROPOSAL');
|
|
181
|
+
// Unknown tools are rejected before the whole completed batch reaches
|
|
182
|
+
// dispatch, not fabricated as recoverable traces with no archive.
|
|
183
|
+
// Validation below waits for final usage/stop before rejecting the batch.
|
|
184
|
+
toolIds.add(event.id);
|
|
185
|
+
const malformed = event.malformed === true || Boolean(event.inputError) || !event.input || typeof event.input !== 'object' || Array.isArray(event.input);
|
|
186
|
+
const call = { id: event.id, name: event.name, input: malformed ? {} : event.input, ...(malformed ? { malformed: true } : {}) };
|
|
187
|
+
size += bytes(call);
|
|
188
|
+
if (size > LIMITS.outputBytes) fail('OUTPUT_LIMIT');
|
|
189
|
+
toolCalls.push(call);
|
|
190
|
+
}
|
|
151
191
|
else if (event.type === 'usage') {
|
|
152
192
|
usage = addUsage(usage, event, params.usageCacheIncluded);
|
|
153
193
|
params.onUsage?.(usage);
|
|
154
194
|
}
|
|
155
195
|
// Hidden thinking, signatures, opaque provider state and raw HTTP exchanges are deliberately not archived.
|
|
156
196
|
}
|
|
197
|
+
if (toolCalls.some(call => call.malformed || !params.tools.some(tool => tool.name === call.name))) fail('INVALID_TOOL_INPUT');
|
|
157
198
|
if (stopReason === 'max_tokens') fail('OUTPUT_TRUNCATED');
|
|
158
|
-
if (stopReason
|
|
159
|
-
|
|
199
|
+
if (!['end_turn', 'tool_use'].includes(stopReason) || (stopReason === 'tool_use') !== Boolean(toolCalls.length)) fail('INVALID_PROPOSAL');
|
|
200
|
+
const result = { text: output, bytes: size, usage, stopReason, ...(toolCalls.length ? { toolCalls } : {}) };
|
|
201
|
+
const state = bindProviderState(providerState, { content: output, toolCalls });
|
|
202
|
+
// Wire continuation only; non-enumerable so repository payloads/public output
|
|
203
|
+
// can never archive hidden reasoning or provider signatures.
|
|
204
|
+
if (state) Object.defineProperty(result, 'continuationState', { value: state });
|
|
205
|
+
return result;
|
|
160
206
|
} catch (error) {
|
|
161
|
-
const safe = error instanceof PersonError ? error : new PersonError('PROVIDER_FAILED');
|
|
207
|
+
const safe = error instanceof PersonError ? error : new PersonError(error?.toolInputRejected === true ? 'INVALID_TOOL_INPUT' : 'PROVIDER_FAILED');
|
|
162
208
|
// This prefix is explicitly incomplete/rejected, never a full accepted record.
|
|
163
|
-
safe.partialOutput = { text: output, retainedBytes: bytes(output)
|
|
209
|
+
safe.partialOutput = { text: output, retainedBytes: bytes(output) + toolCalls.reduce((total, call) => total + bytes(call), 0),
|
|
210
|
+
observedBytes: size, complete: false, accepted: false, usage, stopReason, ...(toolCalls.length ? { toolCalls } : {}) };
|
|
164
211
|
throw safe;
|
|
165
212
|
} finally {
|
|
166
213
|
// Do not let a provider ignoring AbortSignal indefinitely block cancellation/close.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bytes, PROPOSAL_INSTRUCTIONS } from './contracts.js';
|
|
2
|
+
|
|
3
|
+
// Request reserves, not a minimum model window. Runtime still accounts for the
|
|
4
|
+
// actual Soul, state, scripts, attachments and execution transcript per dispatch.
|
|
5
|
+
export const PERSON_ENVELOPE_TOKENS_RESERVED = 1024;
|
|
6
|
+
export const PERSON_FINAL_CONTEXT_RESERVE_BYTES = 16384;
|
|
7
|
+
export const personFinalContextReserveBytes = executionInstructions =>
|
|
8
|
+
Math.max(0, bytes(PROPOSAL_INSTRUCTIONS) - bytes(executionInstructions)) + PERSON_FINAL_CONTEXT_RESERVE_BYTES;
|
|
9
|
+
|
|
10
|
+
/** Static lower bound for the complete native execution schemas and both phases.
|
|
11
|
+
* Schemas come from PersonCapabilities.executionTools(), never a copied list.
|
|
12
|
+
* The service injects this value so the provider has no static tool/runtime imports. */
|
|
13
|
+
export function personMinimumRequestBytes(toolSchemas, executionInstructions) {
|
|
14
|
+
return (toolSchemas.length ? bytes(toolSchemas) : 0) + bytes(executionInstructions) + 2 +
|
|
15
|
+
personFinalContextReserveBytes(executionInstructions);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Standalone provider callers use the same schemas. Lazy imports avoid the
|
|
19
|
+
* provider -> native tools -> Engine dependency cycle during module evaluation. */
|
|
20
|
+
export async function defaultPersonMinimumRequestBytes(config) {
|
|
21
|
+
const [{ PersonCapabilities }, { EXECUTION_INSTRUCTIONS }, { allowedNativeToolIds }] = await Promise.all([
|
|
22
|
+
import('./capabilities.js'), import('./execution-protocol.js'), import('./native-tools.js'),
|
|
23
|
+
]);
|
|
24
|
+
const capabilities = new PersonCapabilities(null, null, { toolHost: { allowedToolIds: () => allowedNativeToolIds(config) } });
|
|
25
|
+
return personMinimumRequestBytes(capabilities.executionTools(), EXECUTION_INSTRUCTIONS);
|
|
26
|
+
}
|
package/yeaft/person/runtime.js
CHANGED
|
@@ -4,18 +4,19 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { utf8PrefixWithinBytes } from '../utf8.js';
|
|
5
5
|
import { CAPABILITY_MAP, capabilityContextBudget, foundationCapabilities, PersonCapabilities, catalogRevision as capabilityCatalogRevision } from './capabilities.js';
|
|
6
6
|
import { attachmentMetadata } from './attachments.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { ExecutionTranscript, EXECUTION_INSTRUCTIONS, proposalDiagnostics } from './execution-protocol.js';
|
|
8
|
+
import { PERSON_ENVELOPE_TOKENS_RESERVED, personFinalContextReserveBytes } from './request-budget.js';
|
|
9
9
|
import { abortable, collectOutput } from './provider.js';
|
|
10
|
+
import { validateCapabilityArguments, validateToolArguments } from './tool-arguments.js';
|
|
10
11
|
import { callUsage, sumUsage } from './turn-diagnostics.js';
|
|
11
|
-
import { bytes, digest, fail,
|
|
12
|
+
import { bytes, digest, fail, FEEDBACK_INSTRUCTIONS, LIMITS, PersonError, PROPOSAL_INSTRUCTIONS, reportedLineage, safeError, validateProposal, validateSelection } from './contracts.js';
|
|
12
13
|
|
|
13
14
|
const messageRef = m => `message:${m.id}:${m.revision}`;
|
|
14
15
|
const conceptRef = c => `concept:${c.id}:${c.revision}`;
|
|
15
|
-
const ENVELOPE_TOKENS_RESERVED =
|
|
16
|
+
const ENVELOPE_TOKENS_RESERVED = PERSON_ENVELOPE_TOKENS_RESERVED;
|
|
16
17
|
|
|
17
18
|
/** Assemble bounded request copies. Omitting a record never deletes or truncates its durable original. */
|
|
18
|
-
export function assembleContext({ snapshot, episode, provider, selection, previous, capabilityResult, taskEvidence, episodeProgress = [], omittedEpisodeProgress = 0, episodeWork = null, executionPattern = null, dependencyRefs = [], activeCapabilities = foundationCapabilities(), capabilityMap = CAPABILITY_MAP, attachments = [], environment, feedback, outputRecovery }) {
|
|
19
|
+
export function assembleContext({ instructions = PROPOSAL_INSTRUCTIONS, toolSchemas = [], reservedContextBytes = 0, snapshot, episode, provider, selection, previous, capabilityResult, taskEvidence, episodeProgress = [], omittedEpisodeProgress = 0, episodeWork = null, executionPattern = null, dependencyRefs = [], activeCapabilities = foundationCapabilities(), capabilityMap = CAPABILITY_MAP, attachments = [], environment, feedback, outputRecovery }) {
|
|
19
20
|
const model = validateSelection(selection, provider.catalog);
|
|
20
21
|
// UTF-8 bytes is a conservative text-token bound; reserve explicit envelope/output overhead.
|
|
21
22
|
const images = attachments.filter(file => file.kind === 'image');
|
|
@@ -28,10 +29,11 @@ export function assembleContext({ snapshot, episode, provider, selection, previo
|
|
|
28
29
|
const imageLabelBytes = imageLabels.reduce((sum, label) => sum + bytes(label), 0);
|
|
29
30
|
// This is a byte allowance derived from the selected model's token window,
|
|
30
31
|
// not a token estimate or an independent fixed-size Person context limit.
|
|
31
|
-
const
|
|
32
|
+
const requestCap = model.contextWindow - model.maxOutput - ENVELOPE_TOKENS_RESERVED - imageTokensReserved;
|
|
33
|
+
const contextCap = requestCap - (toolSchemas.length ? bytes(toolSchemas) : 0) - reservedContextBytes;
|
|
32
34
|
if (!Number.isSafeInteger(model.contextWindow) || !Number.isSafeInteger(model.maxOutput) || model.maxOutput < 1 ||
|
|
33
35
|
!Number.isSafeInteger(contextCap) || contextCap < 1) fail('CONTEXT_LIMIT');
|
|
34
|
-
const system = `${snapshot.person.soul}\n\n${
|
|
36
|
+
const system = `${snapshot.person.soul}\n\n${instructions}`;
|
|
35
37
|
// Preserve the full proposal in durable traces, but do not duplicate optional
|
|
36
38
|
// working notes inside the mandatory previous-proposal context. They have one
|
|
37
39
|
// separately budgeted episodeWork slot below, including explicit null clearing.
|
|
@@ -96,7 +98,7 @@ export function assembleContext({ snapshot, episode, provider, selection, previo
|
|
|
96
98
|
if (!fits()) delete context.outputRecovery;
|
|
97
99
|
}
|
|
98
100
|
context.outputInstructions = 'Fit complete JSON/tool args within budget.maxOutputTokens (reasoning included). Split big scripts or use SpawnAgent; long Shell work uses background:true and explicit result collection. For authorized edits, read relevant ranges, edit, inspect diff/checks. Missing broader validation need not block independently testable work. Recover omitted output via Execution.read or narrow reads, not reruns. Repeated evidence is not progress; diagnostics impose no quota/forced write. Optional work:null|{goal:string,findings:[{summary:string,sourceRefs:string[]}],nextStep:string,blocker:string}. Max 6 findings, summary<=600 UTF-8 bytes, 1..4 exact evidence refs each; other strings<=1000 bytes, total<=6144. Retain relevant findings, retract errors, integrate results before next action.';
|
|
99
|
-
if (!fits()) delete context.outputInstructions;
|
|
101
|
+
if (instructions === EXECUTION_INSTRUCTIONS || !fits()) delete context.outputInstructions;
|
|
100
102
|
// Feedback is a request-only hint, never grounds to fail the next call.
|
|
101
103
|
// Keep durable replies intact; avoid duplicating up to 8 KiB from previousProposal.
|
|
102
104
|
if (feedback) {
|
|
@@ -170,7 +172,7 @@ export function assembleContext({ snapshot, episode, provider, selection, previo
|
|
|
170
172
|
manifest: { stateVersion: snapshot.state.version, sourceRefs: [...sourceRefs], renderedSourceRefs: [...renderedRefs], inputDependencyRefs: [...sourceRefs], omitted,
|
|
171
173
|
boundedRecentWindow: { messages: 12, recentConcepts: 12, focusedConcepts: 12 },
|
|
172
174
|
attachments: attachments.map(attachmentMetadata), imageTokensReserved, imageBudget: images.length ? imageBudget : null,
|
|
173
|
-
contextBytes: bytes(system) + bytes(context) + imageLabelBytes, contextBudgetBytes:
|
|
175
|
+
contextBytes: bytes(system) + bytes(context) + imageLabelBytes, contextBudgetBytes: requestCap, toolSchemaBytes: toolSchemas.length ? bytes(toolSchemas) : 0,
|
|
174
176
|
contextWindowTokens: model.contextWindow, outputTokensReserved: model.maxOutput, envelopeTokensReserved: ENVELOPE_TOKENS_RESERVED,
|
|
175
177
|
// Counts describe actual rendered sources, not the activity label or the
|
|
176
178
|
// inherited bounded provenance read-set. Recall duplicates are counted in its page,
|
|
@@ -229,6 +231,10 @@ export function executionReceipt(callId, capability, result) {
|
|
|
229
231
|
return receipt;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
const archiveExecutionMessages = messages => messages.map(({ providerState, ...message }) => Array.isArray(message.content)
|
|
235
|
+
? { ...message, content: message.content.map(block => block.type === 'image'
|
|
236
|
+
? { type: 'image', archived: false, reason: 'attachment-owned-by-episode' } : block) } : message);
|
|
237
|
+
|
|
232
238
|
export class PersonRuntime {
|
|
233
239
|
constructor({ repository, getProvider, workDir, yeaftDir, config, namespace = repository?.namespace ?? 'default' }) {
|
|
234
240
|
this.repository = repository; this.getProvider = getProvider; this.budget = { calls: null };
|
|
@@ -366,8 +372,6 @@ export class PersonRuntime {
|
|
|
366
372
|
loops: 0, model: null, capability: null, models: new Map(), currentCall: null,
|
|
367
373
|
controlWrites: Promise.resolve(), pauseGate: null, releasePause: null }) {
|
|
368
374
|
const { signal } = controller;
|
|
369
|
-
let lastReplyAt = new Date(episode.createdAt).getTime() || Date.now();
|
|
370
|
-
let lastReply = null;
|
|
371
375
|
let heartbeatRunning = false;
|
|
372
376
|
const heartbeat = setInterval(async () => {
|
|
373
377
|
if (heartbeatRunning || signal.aborted) return;
|
|
@@ -402,57 +406,53 @@ export class PersonRuntime {
|
|
|
402
406
|
selection: toolSelection, effortDecision: toolEffortDecision, parentToolRegistry: this.parentToolRegistry }),
|
|
403
407
|
onResult: result => finalizeNativeResult(result) });
|
|
404
408
|
const capabilities = new PersonCapabilities(this.repository, episode.ownerId, { experience: snapshot.capabilityExperience, triggerKind: episode.kind, created, episode, toolHost, workDir: this.toolOptions.workDir });
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const readConcepts = new Map(), readSources = new Map();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
elapsedSinceReplyMs: Math.max(0, Date.now() - lastReplyAt),
|
|
418
|
-
due: Date.now() - lastReplyAt >= FEEDBACK.minMs, lastReply,
|
|
419
|
-
}, outputRecovery });
|
|
420
|
-
capabilities.activate(context.activeCapabilities);
|
|
421
|
-
const workRefs = episodeWork?.findings.flatMap(finding => finding.sourceRefs) ?? [];
|
|
422
|
-
dependencyRefs = [...new Set([...context.manifest.inputDependencyRefs.filter(ref => !workRefs.includes(ref)), ...workRefs.filter(ref => context.sourceRefs.has(ref))])].slice(-128);
|
|
423
|
-
for (const [id, concept] of context.concepts) { readConcepts.delete(id); readConcepts.set(id, concept); }
|
|
424
|
-
for (const [ref, source] of context.sources) { readSources.delete(ref); readSources.set(ref, source); }
|
|
425
|
-
// Refresh existing reads above, then evict only historical entries. A
|
|
426
|
-
// revision/source rendered in this request must remain valid even when
|
|
427
|
-
// its insertion order overlaps a full inherited window.
|
|
428
|
-
for (const ref of readSources.keys()) {
|
|
429
|
-
if (readSources.size <= 256) break;
|
|
430
|
-
if (!context.sources.has(ref)) readSources.delete(ref);
|
|
409
|
+
const initialTools = capabilities.executionTools();
|
|
410
|
+
const context = assembleContext({ instructions: EXECUTION_INSTRUCTIONS, toolSchemas: initialTools,
|
|
411
|
+
reservedContextBytes: personFinalContextReserveBytes(EXECUTION_INSTRUCTIONS), snapshot, episode, provider, selection,
|
|
412
|
+
attachments, activeCapabilities: [], capabilityMap: capabilities.catalog(), environment: toolHost.environment,
|
|
413
|
+
taskEvidence: projectTaskEvidence(this.tasks()?.snapshot(episode.ownerId)) });
|
|
414
|
+
const transcript = new ExecutionTranscript(context.messages);
|
|
415
|
+
const sourceRefs = new Set(context.sourceRefs), readConcepts = new Map(context.concepts), readSources = new Map(context.sources);
|
|
416
|
+
let phase = 'execution', corrections = 0, inputCorrections = 0, callIndex = 0;
|
|
417
|
+
const observe = result => {
|
|
418
|
+
if (result?.sourceRef) {
|
|
419
|
+
sourceRefs.add(result.sourceRef);
|
|
420
|
+
readSources.set(result.sourceRef, { kind: 'external-tool-observation', reportedSourceRefs: [] });
|
|
431
421
|
}
|
|
432
|
-
for (const
|
|
433
|
-
|
|
434
|
-
|
|
422
|
+
if (result?.kind === 'messages') for (const message of result.items) {
|
|
423
|
+
const ref = messageRef(message); sourceRefs.add(ref);
|
|
424
|
+
readSources.set(ref, { kind: 'message', role: message.role, reportedSourceRefs: message.role === 'user' ? [ref] : [] });
|
|
435
425
|
}
|
|
436
|
-
|
|
426
|
+
if (result?.kind === 'concepts') for (const concept of result.items) {
|
|
427
|
+
const ref = conceptRef(concept); sourceRefs.add(ref); readConcepts.set(concept.id, concept);
|
|
428
|
+
readSources.set(ref, { kind: 'concept', epistemicState: concept.epistemicState,
|
|
429
|
+
reportedSourceRefs: concept.epistemicState === 'reported' ? concept.reportedSourceRefs ?? [] : [] });
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
for (;;) {
|
|
433
|
+
await this.boundary(job, 'preparing');
|
|
434
|
+
const callId = randomUUID();
|
|
435
|
+
const tools = phase === 'execution' ? capabilities.executionTools() : [];
|
|
436
|
+
const system = `${snapshot.person.soul}\n\n${phase === 'execution' ? EXECUTION_INSTRUCTIONS : PROPOSAL_INSTRUCTIONS}`;
|
|
437
|
+
const request = transcript.request({ system, tools, maxBytes: context.manifest.contextBudgetBytes });
|
|
438
|
+
await this.repository.heartbeat(episode);
|
|
437
439
|
const requested = { model: selection.model, effort: selection.effort };
|
|
438
440
|
const effective = { model: selection.model, effort: null, effortObserved: false };
|
|
439
441
|
job.model = selection.model;
|
|
440
442
|
await this.boundary(job, 'model', null, () => this.repository.startCall(episode, {
|
|
441
|
-
callId, callIndex:
|
|
442
|
-
manifest:
|
|
443
|
-
|
|
443
|
+
callId, callIndex: callIndex++, requested, effective, selectionOrigin: selection.origin, reason: selection.reason,
|
|
444
|
+
manifest: { ...context.manifest, protocol: 'native-tool-loop', phase, sourceRefs: [...sourceRefs],
|
|
445
|
+
inputDependencyRefs: [...sourceRefs],
|
|
446
|
+
renderedSourceRefs: [...new Set([...context.manifest.renderedSourceRefs, ...request.renderedSourceRefs])],
|
|
447
|
+
contextSources: { ...context.manifest.contextSources,
|
|
448
|
+
recall: request.recallPages.at(-1) ?? { kind: null, count: 0 }, recallPages: request.recallPages },
|
|
449
|
+
contextBytes: request.contextBytes, omittedExecutionGroups: request.omittedExecutionGroups },
|
|
450
|
+
request: { system, messages: archiveExecutionMessages(request.messages), maxTokens: context.maxTokens, tools },
|
|
444
451
|
}));
|
|
445
452
|
let output, observedEffortDecision;
|
|
446
453
|
try {
|
|
447
|
-
signal.throwIfAborted();
|
|
448
|
-
// collectOutput deliberately exposes only public effort diagnostics.
|
|
449
|
-
// Preserve the complete wire decision privately for child inheritance
|
|
450
|
-
// (including any inherited cap), without archiving provider internals.
|
|
451
454
|
const observingAdapter = { stream: params => provider.adapter.stream({ ...params,
|
|
452
|
-
onEffortDecision: decision => {
|
|
453
|
-
observedEffortDecision = { ...decision };
|
|
454
|
-
params.onEffortDecision?.(decision);
|
|
455
|
-
},
|
|
455
|
+
onEffortDecision: decision => { observedEffortDecision = { ...decision }; params.onEffortDecision?.(decision); },
|
|
456
456
|
}) };
|
|
457
457
|
output = await this.boundary(job, 'model', null, () => {
|
|
458
458
|
job.loops++;
|
|
@@ -460,10 +460,11 @@ export class PersonRuntime {
|
|
|
460
460
|
calls: 0, completedCalls: 0, failedCalls: 0, elapsedMs: 0, usage: null });
|
|
461
461
|
job.models.get(selection.model).calls++;
|
|
462
462
|
job.currentCall = { model: selection.model, startedAt: Date.now(), usage: null };
|
|
463
|
-
return collectOutput(observingAdapter, {
|
|
464
|
-
model: selection.model, effort: selection.effort ?? undefined, effortSource: 'auto',
|
|
463
|
+
return collectOutput(observingAdapter, { model: selection.model, effort: selection.effort ?? undefined, effortSource: 'auto',
|
|
465
464
|
usageCacheIncluded: provider.catalog.find(model => model.id === selection.model)?.usageCacheIncluded,
|
|
466
|
-
system
|
|
465
|
+
system, messages: request.messages, tools, maxTokens: context.maxTokens, signal,
|
|
466
|
+
requestIdentity: { instanceScope: this.toolOptions.yeaftDir ?? this.namespace, ownerScope: episode.ownerId,
|
|
467
|
+
sessionId: `person:${snapshot.person.id}`, vpId: snapshot.person.id, threadId: episode.id },
|
|
467
468
|
onUsage: usage => { if (job.currentCall) job.currentCall.usage = usage; },
|
|
468
469
|
}, decision => {
|
|
469
470
|
effective.effort = decision.effective; effective.effortObserved = true;
|
|
@@ -471,112 +472,124 @@ export class PersonRuntime {
|
|
|
471
472
|
});
|
|
472
473
|
});
|
|
473
474
|
} catch (error) {
|
|
474
|
-
const requestElapsedMs = this.finishUsage(job, false);
|
|
475
|
-
const
|
|
476
|
-
const finalized = await this.repository.finalizeCall(episode, { callId, effective, requestElapsedMs, code: safe.code, output:
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
instruction: 'The truncated response was rejected; no action from it executed. Produce a fresh smaller complete proposal within budget.maxOutputTokens. Split large code into small steps or delegate; do not repeat prior successful tools. Keep the same output allowance; recovery is attempted only once.' };
|
|
485
|
-
selection = { ...selection, origin: 'output-recovery', reason: 'Replan once after output token truncation; no rejected action executed.' };
|
|
475
|
+
const requestElapsedMs = this.finishUsage(job, false), safe = safeError(error, 'PROVIDER_FAILED');
|
|
476
|
+
const partialOutput = error.partialOutput ? { ...error.partialOutput, protocol: phase === 'execution' ? 'native-tools' : 'cognition' } : undefined;
|
|
477
|
+
const finalized = await this.repository.finalizeCall(episode, { callId, effective, requestElapsedMs, code: safe.code, output: partialOutput }).catch(() => false);
|
|
478
|
+
// Collector has not dispatched anything from an incomplete response.
|
|
479
|
+
// Only typed malformed-input rejection or truncation is recoverable.
|
|
480
|
+
if (finalized && !signal.aborted && ['OUTPUT_TRUNCATED', 'INVALID_TOOL_INPUT'].includes(safe.code) && inputCorrections++ < 2) {
|
|
481
|
+
transcript.instruct(JSON.stringify({ code: safe.code, rejectedBeforeExecution: true,
|
|
482
|
+
instruction: phase === 'cognition'
|
|
483
|
+
? 'Return smaller complete final cognitive JSON with next:null; tools remain disabled. Do not repeat prior completed operations.'
|
|
484
|
+
: 'No tools in that rejected response executed. Return smaller complete tool arguments or public result. Do not repeat prior completed operations.' }));
|
|
486
485
|
continue;
|
|
487
486
|
}
|
|
488
487
|
throw safe;
|
|
489
488
|
}
|
|
490
489
|
const requestElapsedMs = this.finishUsage(job, true);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (!await this.repository.finalizeCall(episode, { callId, effective, output, requestElapsedMs })) fail('STALE');
|
|
494
|
-
outputRecovery = null;
|
|
490
|
+
if (!await this.repository.finalizeCall(episode, { callId, effective,
|
|
491
|
+
output: { ...output, protocol: phase === 'execution' ? 'native-tools' : 'cognition' }, requestElapsedMs })) fail('STALE');
|
|
495
492
|
await this.boundary(job, 'validating');
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
493
|
+
if (phase === 'cognition') {
|
|
494
|
+
let proposal;
|
|
495
|
+
try {
|
|
496
|
+
try { proposal = JSON.parse(output.text); } catch { fail('INVALID_PROPOSAL'); }
|
|
497
|
+
validateProposal(proposal, { stateVersion: episode.baseStateVersion, sourceRefs, concepts: readConcepts,
|
|
498
|
+
sources: readSources, catalog: provider.catalog, dream: episode.kind === 'dream' });
|
|
499
|
+
} catch (error) {
|
|
500
|
+
const safe = safeError(error, 'INVALID_PROPOSAL');
|
|
501
|
+
const diagnostics = proposalDiagnostics(proposal, { sourceRefs, concepts: readConcepts });
|
|
502
|
+
await this.repository.append(episode, 'proposal_rejected', { callId, code: safe.code, diagnostics });
|
|
503
|
+
if (safe.code !== 'INVALID_PROPOSAL' || corrections++ >= 2) throw safe;
|
|
504
|
+
transcript.response(output);
|
|
505
|
+
transcript.instruct(JSON.stringify({ phase: 'cognition-correction', diagnostics,
|
|
506
|
+
instruction: 'Correct only the final public cognitive JSON. next:null; tools remain disabled and completed operations must not replay. Do not invent refs or weaken reported lineage.' }));
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
await this.repository.append(episode, 'activity', { callId, activity: proposal.activity, decision: proposal.decision, disposition: 'proposed-commit' });
|
|
510
|
+
await this.boundary(job, 'committing', null, () => this.repository.commit(episode, proposal, selection, callId,
|
|
511
|
+
new Map(proposal.concepts.map(c => [c.id, c.epistemicState === 'reported' ? reportedLineage(c, readSources) : []]))));
|
|
509
512
|
return;
|
|
510
513
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
514
|
+
const group = transcript.response(output);
|
|
515
|
+
// Native public text is already drained and durably bound to this exact
|
|
516
|
+
// call. Publish useful progress at the existing cadence, without another
|
|
517
|
+
// model request or a cognitive proposal between tool steps.
|
|
518
|
+
let delivery = null;
|
|
519
|
+
if (output.text.trim() && this.repository.publishProgress) {
|
|
520
|
+
const final = !output.toolCalls?.length;
|
|
521
|
+
delivery = await this.boundary(job, 'validating', null, () => this.repository.publishProgress(episode, { callId, reply: output.text, final }));
|
|
516
522
|
}
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
523
|
+
if (!output.toolCalls?.length) {
|
|
524
|
+
phase = 'cognition';
|
|
525
|
+
transcript.instruct(JSON.stringify({ phase: 'final-cognition', baseStateVersion: episode.baseStateVersion,
|
|
526
|
+
...(delivery ? { executionDelivery: { messageId: delivery.id, alreadyPublished: true } } : {}),
|
|
527
|
+
sourceRefs: [...sourceRefs], instruction: 'Execution has ended. Distill once into the final strict cognitive JSON; next:null. Preserve actual results and unfinished blockers. The native public result is already delivered when executionDelivery is present; use reply:null unless a substantive correction or additional delivery is needed. Tool observations never establish user-reported lineage.' }));
|
|
528
|
+
continue;
|
|
520
529
|
}
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
530
|
+
// Delegation inherits the producing request's observed, representable
|
|
531
|
+
// effort rather than an obsolete proposal.next selection. The child
|
|
532
|
+
// boundary still pins the admitted model and applies its own ceiling.
|
|
533
|
+
const modelEfforts = provider.catalog.find(model => model.id === requested.model)?.efforts ?? [];
|
|
534
|
+
toolSelection = { ...requested, effort: effective.effortObserved && modelEfforts.includes(effective.effort) ? effective.effort : requested.effort };
|
|
535
|
+
toolEffortDecision = { requested: requested.effort, effective: effective.effort, model: requested.model,
|
|
536
|
+
source: 'auto', wireMode: effective.wireMode ?? 'omitted', thinkingEnabled: effective.thinkingEnabled === true, ...observedEffortDecision };
|
|
537
|
+
for (let toolIndex = 0; toolIndex < output.toolCalls.length; toolIndex++) {
|
|
538
|
+
const toolCall = output.toolCalls[toolIndex], traceCallId = randomUUID();
|
|
539
|
+
let invocation, inputError = null;
|
|
540
|
+
try {
|
|
541
|
+
invocation = capabilities.invocation(toolCall.name, toolCall.input);
|
|
542
|
+
const schema = tools.find(tool => tool.name === toolCall.name)?.parameters;
|
|
543
|
+
if (!schema) fail('UNSUPPORTED');
|
|
544
|
+
try {
|
|
545
|
+
if (!toolCall.malformed) {
|
|
546
|
+
validateToolArguments(toolCall.input, schema);
|
|
547
|
+
validateCapabilityArguments(invocation);
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
const safe = safeError(error, 'INVALID_TOOL_INPUT');
|
|
551
|
+
if (safe.code !== 'INVALID_TOOL_INPUT') throw error;
|
|
552
|
+
inputError = safe;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
// Collector already rejected unknown names before any batch effect.
|
|
557
|
+
// A catalog/authority mismatch at this boundary is not retry-safe.
|
|
558
|
+
throw safeError(error, 'UNSUPPORTED');
|
|
559
|
+
}
|
|
539
560
|
const manifest = capabilities.executionManifest(invocation.id);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
561
|
+
await this.boundary(job, 'capability', invocation.id, () => this.repository.startCapability(episode, {
|
|
562
|
+
callId: traceCallId, modelCallId: callId, toolUseId: toolCall.id, toolIndex, capability: invocation,
|
|
563
|
+
capabilityManifest: manifest, access: capabilities.active.get(invocation.id)?.access,
|
|
564
|
+
}));
|
|
565
|
+
let result, finalized = false;
|
|
566
|
+
const finalize = async (value, code) => {
|
|
567
|
+
const accepted = await this.repository.finalizeCapability(episode, { callId: traceCallId, capability: invocation, result: value, code,
|
|
546
568
|
terminalCode: signal.aborted ? safeError(signal.reason, 'INTERRUPTED').code : null });
|
|
547
|
-
finalized = true;
|
|
548
|
-
return accepted;
|
|
549
|
-
};
|
|
550
|
-
finalizeNativeResult = async result => {
|
|
551
|
-
capabilityResult = result;
|
|
552
|
-
if (!await finalize(result) && !signal.aborted) fail('STALE');
|
|
569
|
+
finalized = true; return accepted;
|
|
553
570
|
};
|
|
571
|
+
finalizeNativeResult = async value => { result = value; if (!await finalize(value) && !signal.aborted) fail('STALE'); };
|
|
554
572
|
try {
|
|
555
573
|
signal.throwIfAborted();
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
});
|
|
562
|
-
if (!finalized && !await finalize(capabilityResult)) fail('STALE');
|
|
563
|
-
if (invocation.id === 'PresentResponse' && capabilityResult?.published) {
|
|
564
|
-
lastReply = capabilityResult.summary;
|
|
565
|
-
lastReplyAt = new Date(capabilityResult.createdAt).getTime();
|
|
574
|
+
if (toolCall.malformed || inputError) {
|
|
575
|
+
result = { ok: false, code: 'INVALID_TOOL_INPUT', errorEffect: 'none', replaySafe: true, output: 'Malformed or schema-invalid provider tool arguments. Nothing executed; correct only the arguments.' };
|
|
576
|
+
if (inputCorrections++ >= 2) fail('INVALID_TOOL_INPUT');
|
|
577
|
+
} else {
|
|
578
|
+
result = await this.boundary(job, 'capability', invocation.id, () => capabilities.execute(invocation, { signal, callId: traceCallId }));
|
|
566
579
|
}
|
|
580
|
+
if (!finalized && !await finalize(result)) fail('STALE');
|
|
567
581
|
signal.throwIfAborted();
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (isNativeTool(invocation.id)) capabilityResult = projectNativeResult(capabilityResult);
|
|
582
|
+
// A confirmed nonzero exit is useful failure evidence (for example a
|
|
583
|
+
// failing test), not proof of rollback or permission to replay it.
|
|
584
|
+
// Only unconfirmed process/effect termination blocks the next step.
|
|
585
|
+
if (result?.terminal) fail('TOOL_EFFECT_UNCONFIRMED');
|
|
586
|
+
const projection = transcript.result(group, toolCall.id, traceCallId, invocation, result);
|
|
587
|
+
// Recall pages carry structured message/concept reads; if projection
|
|
588
|
+
// cannot retain them it must not manufacture their provenance.
|
|
589
|
+
observe(projection);
|
|
577
590
|
} catch (error) {
|
|
578
591
|
const safe = safeError(error, 'UNSUPPORTED');
|
|
579
|
-
if (!finalized) await finalize(
|
|
592
|
+
if (!finalized) await finalize(result, safe.code).catch(() => {});
|
|
580
593
|
throw safe;
|
|
581
594
|
} finally { finalizeNativeResult = null; }
|
|
582
595
|
}
|