@lensagent/core 0.1.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 +48 -0
- package/config/lens.example.jsonc +92 -0
- package/config/pair.example.json +3 -0
- package/dist/adapters/executor.d.ts +35 -0
- package/dist/adapters/executor.js +296 -0
- package/dist/adapters/lsp.d.ts +18 -0
- package/dist/adapters/lsp.js +218 -0
- package/dist/adapters/mcp.d.ts +32 -0
- package/dist/adapters/mcp.js +283 -0
- package/dist/adapters/revert.d.ts +12 -0
- package/dist/adapters/revert.js +155 -0
- package/dist/config/runtime.d.ts +55 -0
- package/dist/config/runtime.js +371 -0
- package/dist/config/schema.d.ts +292 -0
- package/dist/config/schema.js +177 -0
- package/dist/contracts/adapter.d.ts +28 -0
- package/dist/contracts/adapter.js +8 -0
- package/dist/contracts/capability.d.ts +41 -0
- package/dist/contracts/capability.js +1 -0
- package/dist/contracts/domain.d.ts +61 -0
- package/dist/contracts/domain.js +1 -0
- package/dist/contracts/index.d.ts +5 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/model.d.ts +77 -0
- package/dist/contracts/model.js +1 -0
- package/dist/contracts/ports.d.ts +14 -0
- package/dist/contracts/ports.js +14 -0
- package/dist/core/access.d.ts +24 -0
- package/dist/core/access.js +205 -0
- package/dist/core/codec.d.ts +33 -0
- package/dist/core/codec.js +105 -0
- package/dist/core/domain.d.ts +13 -0
- package/dist/core/domain.js +24 -0
- package/dist/core/host.d.ts +19 -0
- package/dist/core/host.js +249 -0
- package/dist/core/identity.d.ts +19 -0
- package/dist/core/identity.js +54 -0
- package/dist/entry/bun.d.ts +1 -0
- package/dist/entry/bun.js +2 -0
- package/dist/entry/node.d.ts +2 -0
- package/dist/entry/node.js +3 -0
- package/dist/entry/rebuild.d.ts +1 -0
- package/dist/entry/rebuild.js +47 -0
- package/dist/entry/run.d.ts +1 -0
- package/dist/entry/run.js +43 -0
- package/dist/execution/lsp-process.d.ts +14 -0
- package/dist/execution/lsp-process.js +234 -0
- package/dist/http/server.d.ts +34 -0
- package/dist/http/server.js +1332 -0
- package/dist/launch.d.ts +3 -0
- package/dist/launch.js +4 -0
- package/dist/runtime/agent.d.ts +35 -0
- package/dist/runtime/agent.js +853 -0
- package/dist/runtime/artifacts.d.ts +15 -0
- package/dist/runtime/artifacts.js +72 -0
- package/dist/runtime/cache.d.ts +20 -0
- package/dist/runtime/cache.js +44 -0
- package/dist/runtime/calls.d.ts +21 -0
- package/dist/runtime/calls.js +130 -0
- package/dist/runtime/capabilities.d.ts +58 -0
- package/dist/runtime/capabilities.js +196 -0
- package/dist/runtime/cleanup.d.ts +7 -0
- package/dist/runtime/cleanup.js +33 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +169 -0
- package/dist/runtime/diagnostics.d.ts +8 -0
- package/dist/runtime/diagnostics.js +71 -0
- package/dist/runtime/events.d.ts +22 -0
- package/dist/runtime/events.js +131 -0
- package/dist/runtime/kernel-capabilities.d.ts +32 -0
- package/dist/runtime/kernel-capabilities.js +200 -0
- package/dist/runtime/manifests.d.ts +14 -0
- package/dist/runtime/manifests.js +117 -0
- package/dist/runtime/model.d.ts +20 -0
- package/dist/runtime/model.js +241 -0
- package/dist/runtime/observability.d.ts +22 -0
- package/dist/runtime/observability.js +104 -0
- package/dist/runtime/policy.d.ts +8 -0
- package/dist/runtime/policy.js +31 -0
- package/dist/runtime/profile.d.ts +8 -0
- package/dist/runtime/profile.js +52 -0
- package/dist/runtime/projects.d.ts +40 -0
- package/dist/runtime/projects.js +115 -0
- package/dist/runtime/results.d.ts +7 -0
- package/dist/runtime/results.js +55 -0
- package/dist/runtime/review.d.ts +9 -0
- package/dist/runtime/review.js +54 -0
- package/dist/runtime/scheduler.d.ts +15 -0
- package/dist/runtime/scheduler.js +85 -0
- package/dist/runtime/skills.d.ts +2 -0
- package/dist/runtime/skills.js +83 -0
- package/dist/runtime/tokenizer.d.ts +8 -0
- package/dist/runtime/tokenizer.js +30 -0
- package/dist/store/database.d.ts +3 -0
- package/dist/store/database.js +49 -0
- package/dist/store/event-log.d.ts +19 -0
- package/dist/store/event-log.js +61 -0
- package/dist/store/schema.d.ts +1 -0
- package/dist/store/schema.js +347 -0
- package/dist/store/store.d.ts +344 -0
- package/dist/store/store.js +2554 -0
- package/package.json +73 -0
- package/proto/executor.proto +314 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import { jsonValue } from '../core/codec.js';
|
|
2
|
+
import { fail } from '../core/domain.js';
|
|
3
|
+
import { claimsConflict } from './capabilities.js';
|
|
4
|
+
import { decidePolicy, policyPrompt } from './policy.js';
|
|
5
|
+
import { planCache } from './cache.js';
|
|
6
|
+
import { resolveReasoningEffort } from './profile.js';
|
|
7
|
+
import { estimateTokens } from './tokenizer.js';
|
|
8
|
+
import { derivePreparationDiagnostics } from './diagnostics.js';
|
|
9
|
+
export class AgentRuntime {
|
|
10
|
+
store;
|
|
11
|
+
config;
|
|
12
|
+
context;
|
|
13
|
+
gateway;
|
|
14
|
+
registry;
|
|
15
|
+
calls;
|
|
16
|
+
reviewer;
|
|
17
|
+
events;
|
|
18
|
+
#controllers = new Map();
|
|
19
|
+
#running = new Map();
|
|
20
|
+
constructor(store, config, context, gateway, registry, calls, reviewer, events) {
|
|
21
|
+
this.store = store;
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.context = context;
|
|
24
|
+
this.gateway = gateway;
|
|
25
|
+
this.registry = registry;
|
|
26
|
+
this.calls = calls;
|
|
27
|
+
this.reviewer = reviewer;
|
|
28
|
+
this.events = events;
|
|
29
|
+
}
|
|
30
|
+
wake(projectId, agentId) {
|
|
31
|
+
const key = `${projectId}:${agentId}`;
|
|
32
|
+
if (this.#running.has(key))
|
|
33
|
+
return;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
this.#controllers.set(key, controller);
|
|
36
|
+
const running = this.#runAvailable(projectId, agentId, controller.signal)
|
|
37
|
+
.catch((error) => {
|
|
38
|
+
if (!controller.signal.aborted)
|
|
39
|
+
this.#terminateCurrent(projectId, agentId, error);
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
this.#running.delete(key);
|
|
43
|
+
this.#controllers.delete(key);
|
|
44
|
+
});
|
|
45
|
+
this.#running.set(key, running);
|
|
46
|
+
}
|
|
47
|
+
cancel(projectId, agentId) {
|
|
48
|
+
for (const callId of this.store.activeCallIds(projectId, agentId)) {
|
|
49
|
+
const call = this.store.getCall(projectId, callId);
|
|
50
|
+
if (call.state !== 'TERMINAL')
|
|
51
|
+
this.calls.cancel(projectId, callId);
|
|
52
|
+
}
|
|
53
|
+
this.store.cancelAgent(projectId, agentId);
|
|
54
|
+
this.#controllers.get(`${projectId}:${agentId}`)?.abort(new Error('Agent cancelled.'));
|
|
55
|
+
}
|
|
56
|
+
async close() {
|
|
57
|
+
for (const controller of this.#controllers.values())
|
|
58
|
+
controller.abort(new Error('Agent runtime closed.'));
|
|
59
|
+
await Promise.allSettled(this.#running.values());
|
|
60
|
+
}
|
|
61
|
+
async compact(projectId, agentId, expectedContextRevision, pivotTurnGroupId, signal) {
|
|
62
|
+
const agent = this.store.getAgent(projectId, agentId);
|
|
63
|
+
if (agent.state !== 'IDLE')
|
|
64
|
+
fail('AGENT_BUSY', 409, 'The Agent must be idle before manual Compaction.');
|
|
65
|
+
const snapshot = this.config.current;
|
|
66
|
+
const selected = this.gateway.resolve(string(agent.model), snapshot.config);
|
|
67
|
+
const prepared = this.context.prepare(projectId, agentId, snapshot.config, selected.contextWindowTokens, selected.profile);
|
|
68
|
+
if (prepared.sourceRevision !== expectedContextRevision)
|
|
69
|
+
fail('REVISION_CONFLICT', 409, 'The Context changed before Compaction.');
|
|
70
|
+
const preserveFrom = pivotTurnGroupId === undefined ? undefined : preserveAfterGroup(prepared.source.items, pivotTurnGroupId);
|
|
71
|
+
return this.#compact(projectId, agentId, null, prepared, snapshot, signal, preserveFrom);
|
|
72
|
+
}
|
|
73
|
+
async createSubagent(projectId, input, signal) {
|
|
74
|
+
const parent = this.store.getAgent(projectId, input.parentAgentId);
|
|
75
|
+
const snapshot = this.config.current;
|
|
76
|
+
const role = snapshot.config.roles.subagent;
|
|
77
|
+
const model = input.model ?? role.model;
|
|
78
|
+
const modelReasoningEffort = input.modelReasoningEffort ?? role.modelReasoningEffort;
|
|
79
|
+
if (model === null || modelReasoningEffort === null || role.policyPreset === null)
|
|
80
|
+
fail('CONFIG_INVALID', 503, 'The subagent role is incomplete.');
|
|
81
|
+
const selected = this.gateway.resolve(model, snapshot.config);
|
|
82
|
+
resolveReasoningEffort(selected.profile, modelReasoningEffort);
|
|
83
|
+
const policies = snapshot.config.policyPresets[role.policyPreset]?.capabilities;
|
|
84
|
+
if (policies === undefined)
|
|
85
|
+
fail('POLICY_PRESET_MISSING', 409, 'The Subagent Policy preset does not exist.');
|
|
86
|
+
const capabilityIds = [...new Set([
|
|
87
|
+
...this.registry.list().filter((definition) => definition.defaultAssignment === 'all_new_agents').map((definition) => definition.id),
|
|
88
|
+
...role.capabilities
|
|
89
|
+
])];
|
|
90
|
+
let compactSummary;
|
|
91
|
+
if (input.forkContext === 'compact') {
|
|
92
|
+
const parentModel = this.gateway.resolve(string(parent.model), snapshot.config);
|
|
93
|
+
const prepared = this.context.prepare(projectId, input.parentAgentId, snapshot.config, parentModel.contextWindowTokens, parentModel.profile);
|
|
94
|
+
compactSummary = await this.#summarize(projectId, input.parentAgentId, null, prepared, snapshot, signal);
|
|
95
|
+
}
|
|
96
|
+
const created = this.store.createSubagent(projectId, input.parentAgentId, {
|
|
97
|
+
name: input.name,
|
|
98
|
+
model,
|
|
99
|
+
modelReasoningEffort,
|
|
100
|
+
policyPreset: role.policyPreset,
|
|
101
|
+
capabilities: Object.fromEntries(capabilityIds.map((capabilityId) => {
|
|
102
|
+
const definition = this.registry.get(capabilityId);
|
|
103
|
+
return [capabilityId, policies[capabilityId] ?? (definition.effect === 'read_only' ? 'read_only' : 'ask')];
|
|
104
|
+
})),
|
|
105
|
+
loadedCapabilities: capabilityIds.filter((capabilityId) => this.registry.get(capabilityId).loadMode === 'eager'),
|
|
106
|
+
forkContext: input.forkContext,
|
|
107
|
+
...(compactSummary === undefined ? {} : { compactSummary }),
|
|
108
|
+
...(input.initialPrompt === undefined ? {} : { initialPrompt: input.initialPrompt }),
|
|
109
|
+
...(input.initialMessage === undefined ? {} : { initialMessage: input.initialMessage }),
|
|
110
|
+
...(input.ingress === undefined ? {} : { ingress: input.ingress })
|
|
111
|
+
});
|
|
112
|
+
const agent = created.agent;
|
|
113
|
+
if (jsonObject(agent) && typeof agent.id === 'string' && created.run !== null)
|
|
114
|
+
this.wake(projectId, agent.id);
|
|
115
|
+
return created;
|
|
116
|
+
}
|
|
117
|
+
async #runAvailable(projectId, agentId, signal) {
|
|
118
|
+
for (;;) {
|
|
119
|
+
const claimed = this.store.claimRun(projectId, agentId);
|
|
120
|
+
if (claimed === null)
|
|
121
|
+
return;
|
|
122
|
+
const pendingIds = claimed.pending.map((entry) => string(entry.id));
|
|
123
|
+
let stepId = string(claimed.step.id);
|
|
124
|
+
let pendingConsumed = false;
|
|
125
|
+
try {
|
|
126
|
+
for (let stepCount = 0; stepCount < 64; stepCount += 1) {
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
const agent = this.store.getAgent(projectId, agentId);
|
|
129
|
+
const configSnapshot = this.config.current;
|
|
130
|
+
const modelReference = string(agent.model);
|
|
131
|
+
const resolved = this.gateway.resolve(modelReference, configSnapshot.config);
|
|
132
|
+
let prepared = this.context.prepare(projectId, agentId, configSnapshot.config, resolved.contextWindowTokens, resolved.profile);
|
|
133
|
+
if (prepared.estimate.safe > inputLimit(configSnapshot, resolved.contextWindowTokens)) {
|
|
134
|
+
await this.#compact(projectId, agentId, stepId, prepared, configSnapshot, signal);
|
|
135
|
+
prepared = this.context.prepare(projectId, agentId, configSnapshot.config, resolved.contextWindowTokens, resolved.profile);
|
|
136
|
+
if (prepared.estimate.safe > inputLimit(configSnapshot, resolved.contextWindowTokens))
|
|
137
|
+
fail('CONTEXT_TOO_LARGE', 413, 'Context remains over the model input limit after Compaction.');
|
|
138
|
+
}
|
|
139
|
+
if (!pendingConsumed) {
|
|
140
|
+
for (const pendingId of pendingIds)
|
|
141
|
+
this.store.consumePending(projectId, pendingId);
|
|
142
|
+
pendingConsumed = true;
|
|
143
|
+
}
|
|
144
|
+
const sample = await this.#sample(projectId, agent, stepId, prepared, configSnapshot, signal);
|
|
145
|
+
if (sample.text.length > 0)
|
|
146
|
+
this.store.appendAssistantMessage(projectId, agentId, stepId, sample.text);
|
|
147
|
+
if (sample.tools.length === 0) {
|
|
148
|
+
this.store.finishStep(projectId, stepId, 'success');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
this.store.appendToolCalls(projectId, agentId, stepId, sample.tools.map((entry) => ({
|
|
152
|
+
id: entry.providerId,
|
|
153
|
+
name: entry.name,
|
|
154
|
+
arguments: entry.contextArguments ?? entry.arguments
|
|
155
|
+
})));
|
|
156
|
+
const callIds = await this.#materializeCalls(projectId, agent, stepId, sample, signal);
|
|
157
|
+
await this.#executePartitions(projectId, callIds, signal);
|
|
158
|
+
this.store.yieldResults(projectId, agentId, callIds);
|
|
159
|
+
stepId = string(this.store.continueRun(projectId, stepId).id);
|
|
160
|
+
}
|
|
161
|
+
const run = this.store.getRun(projectId, string(claimed.run.id));
|
|
162
|
+
if (run.state === 'RUNNING' || run.state === 'WAITING')
|
|
163
|
+
this.store.finishStep(projectId, stepId, 'failed', 'STEP_LIMIT_EXCEEDED');
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (code(error) === 'COMPACTION_FAILED' && pendingConsumed) {
|
|
167
|
+
for (const pending of [...claimed.pending].reverse())
|
|
168
|
+
this.store.restorePending(projectId, pending);
|
|
169
|
+
}
|
|
170
|
+
const step = this.store.getStep(projectId, stepId);
|
|
171
|
+
if (step.state !== 'TERMINAL') {
|
|
172
|
+
this.store.finishStep(projectId, stepId, signal.aborted ? 'cancelled' : 'failed', code(error), reason(error));
|
|
173
|
+
}
|
|
174
|
+
if (signal.aborted)
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async #sample(projectId, agent, stepId, prepared, configSnapshot, signal, reactiveCompacted = false) {
|
|
180
|
+
const role = agent.parent_agent_id === null ? configSnapshot.config.roles.agent : configSnapshot.config.roles.subagent;
|
|
181
|
+
const primary = string(agent.model);
|
|
182
|
+
const candidates = [
|
|
183
|
+
...Array.from({ length: role.providerRouting.retryMaxAttempts }, () => primary),
|
|
184
|
+
...role.providerRouting.fallback
|
|
185
|
+
];
|
|
186
|
+
const manifestTools = this.#modelTools(projectId, string(agent.id));
|
|
187
|
+
const cacheConfig = configSnapshot.config.providerPromptCache;
|
|
188
|
+
let lastError;
|
|
189
|
+
for (const model of candidates) {
|
|
190
|
+
signal.throwIfAborted();
|
|
191
|
+
const resolved = this.gateway.resolve(model, configSnapshot.config);
|
|
192
|
+
const ptcProfile = resolved.profile.value.ptc;
|
|
193
|
+
const ptcEnabled = resolved.programmaticToolCallingSupported
|
|
194
|
+
&& ptcProfile?.enabledByDefault === true
|
|
195
|
+
&& manifestTools.some((entry) => entry.id === 'programmatic_tool_run');
|
|
196
|
+
const tools = ptcEnabled ? manifestTools : manifestTools.filter((entry) => entry.id !== 'programmatic_tool_run');
|
|
197
|
+
const reasoningEffort = resolveReasoningEffort(resolved.profile, string(agent.model_reasoning_effort));
|
|
198
|
+
const maximumOutputTokens = configSnapshot.config.contextBudget?.outputReserveTokens ?? 4096;
|
|
199
|
+
const requestFrame = {
|
|
200
|
+
tools: tools,
|
|
201
|
+
reasoning_effort: reasoningEffort,
|
|
202
|
+
max_output_tokens: maximumOutputTokens,
|
|
203
|
+
provider_visible_options: {
|
|
204
|
+
profile_id: resolved.profile.id,
|
|
205
|
+
profile_version: resolved.profile.value.version
|
|
206
|
+
},
|
|
207
|
+
...(ptcEnabled ? {
|
|
208
|
+
programmatic_tool_calling: {
|
|
209
|
+
capability_ids: tools.map((entry) => entry.id).filter((entry) => entry !== 'programmatic_tool_run' && entry !== 'capability_search'),
|
|
210
|
+
max_tool_calls: ptcProfile.maxToolCalls,
|
|
211
|
+
schema_version: ptcProfile.schemaVersion,
|
|
212
|
+
instruction: ptcProfile.instruction
|
|
213
|
+
}
|
|
214
|
+
} : {})
|
|
215
|
+
};
|
|
216
|
+
const candidateContext = this.context.prepareSource(prepared.source, configSnapshot.config, resolved.contextWindowTokens, resolved.profile, requestFrame);
|
|
217
|
+
if (candidateContext.estimate.safe > inputLimit(configSnapshot, resolved.contextWindowTokens)) {
|
|
218
|
+
lastError = new Error(`Context exceeds the input limit for ${model}.`);
|
|
219
|
+
if (!reactiveCompacted) {
|
|
220
|
+
await this.#compact(projectId, string(agent.id), stepId, candidateContext, configSnapshot, signal);
|
|
221
|
+
const nextModel = this.gateway.resolve(string(agent.model), configSnapshot.config);
|
|
222
|
+
const next = this.context.prepare(projectId, string(agent.id), configSnapshot.config, nextModel.contextWindowTokens, nextModel.profile);
|
|
223
|
+
return this.#sample(projectId, agent, stepId, next, configSnapshot, signal, true);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const messages = ptcEnabled
|
|
228
|
+
? [...candidateContext.messages, { role: 'system', content: ptcProfile.instruction }]
|
|
229
|
+
: candidateContext.messages;
|
|
230
|
+
const cache = planCache(messages, tools, resolved.cacheSupported && (cacheConfig?.enabled ?? resolved.profile.value.cache.enabledByDefault), cacheConfig?.minimumEligiblePrefixTokens ?? resolved.profile.value.cache.minimumEligiblePrefixTokens, cacheConfig?.maximumBreakpoints ?? resolved.profile.value.cache.maximumBreakpoints, resolved.profile.value.cache.segmentStability);
|
|
231
|
+
const requestBase = {
|
|
232
|
+
model,
|
|
233
|
+
messages,
|
|
234
|
+
tools,
|
|
235
|
+
reasoningEffort,
|
|
236
|
+
maxOutputTokens: maximumOutputTokens,
|
|
237
|
+
signal
|
|
238
|
+
};
|
|
239
|
+
const ptc = ptcEnabled ? {
|
|
240
|
+
capabilityIds: tools.map((entry) => entry.id).filter((entry) => entry !== 'programmatic_tool_run' && entry !== 'capability_search'),
|
|
241
|
+
maxToolCalls: ptcProfile.maxToolCalls,
|
|
242
|
+
schemaVersion: ptcProfile.schemaVersion,
|
|
243
|
+
instruction: ptcProfile.instruction
|
|
244
|
+
} : null;
|
|
245
|
+
const attempt = this.store.createAttempt(projectId, {
|
|
246
|
+
agentId: string(agent.id),
|
|
247
|
+
stepId,
|
|
248
|
+
kind: 'agent',
|
|
249
|
+
model,
|
|
250
|
+
profileId: resolved.profile.id,
|
|
251
|
+
profileDigest: resolved.profile.digest,
|
|
252
|
+
profileVersion: resolved.profile.value.version,
|
|
253
|
+
configDigest: configSnapshot.digest,
|
|
254
|
+
contextRevision: candidateContext.sourceRevision,
|
|
255
|
+
request: frozenRequest(requestBase, ptc),
|
|
256
|
+
preparation: {
|
|
257
|
+
context_digest: candidateContext.digest,
|
|
258
|
+
token_estimate: candidateContext.estimate,
|
|
259
|
+
projection_decisions: candidateContext.decisions,
|
|
260
|
+
cache_plan: cache,
|
|
261
|
+
...derivePreparationDiagnostics(candidateContext.source, configSnapshot.config)
|
|
262
|
+
},
|
|
263
|
+
capabilities: tools.map((entry) => ({ id: entry.id, schema: entry.inputSchema }))
|
|
264
|
+
});
|
|
265
|
+
const attemptId = string(attempt.id);
|
|
266
|
+
for (const decision of candidateContext.decisions) {
|
|
267
|
+
this.store.appendProjectionDecision(projectId, string(agent.id), attemptId, decision.stage, decision.sourceTokens, decision.projectedTokens, { context_digest: candidateContext.digest });
|
|
268
|
+
}
|
|
269
|
+
const step = this.store.getStep(projectId, stepId);
|
|
270
|
+
const runId = string(step.run_id);
|
|
271
|
+
let streamSequence = 0;
|
|
272
|
+
const stream = (deltaType, detail) => {
|
|
273
|
+
this.events.emit(projectId, this.store.stateRevision(), 'stream.delta', {
|
|
274
|
+
project_id: projectId,
|
|
275
|
+
task_id: string(agent.task_id),
|
|
276
|
+
session_id: string(agent.session_id),
|
|
277
|
+
agent_id: string(agent.id),
|
|
278
|
+
run_id: runId,
|
|
279
|
+
step_id: stepId,
|
|
280
|
+
attempt_id: attemptId,
|
|
281
|
+
stream_sequence: ++streamSequence,
|
|
282
|
+
delta_type: deltaType,
|
|
283
|
+
...detail
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
const ptcState = ptc === null ? null : {
|
|
287
|
+
attemptId,
|
|
288
|
+
stepId,
|
|
289
|
+
agent,
|
|
290
|
+
capabilityIds: ptc.capabilityIds,
|
|
291
|
+
maxToolCalls: ptc.maxToolCalls,
|
|
292
|
+
providerCalls: new Map(),
|
|
293
|
+
rootCallIds: new Set(),
|
|
294
|
+
nextOrdinal: 0,
|
|
295
|
+
nestedCount: 0
|
|
296
|
+
};
|
|
297
|
+
const request = ptcState === null ? requestBase : {
|
|
298
|
+
...requestBase,
|
|
299
|
+
programmaticToolCalling: {
|
|
300
|
+
capabilityIds: ptcState.capabilityIds,
|
|
301
|
+
maxToolCalls: ptcState.maxToolCalls,
|
|
302
|
+
execute: (input, executionSignal) => this.#executePtcNested(projectId, ptcState, input, executionSignal ?? signal)
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
this.store.transitionAttempt(projectId, attemptId, 'PREPARED', 'RUNNING');
|
|
306
|
+
let text = '';
|
|
307
|
+
const toolBlocks = [];
|
|
308
|
+
let usage = null;
|
|
309
|
+
let contentStarted = false;
|
|
310
|
+
try {
|
|
311
|
+
for await (const event of this.gateway.stream(request, configSnapshot.config)) {
|
|
312
|
+
if (event.type === 'text') {
|
|
313
|
+
contentStarted = true;
|
|
314
|
+
text += event.text;
|
|
315
|
+
stream('text', { text: event.text });
|
|
316
|
+
}
|
|
317
|
+
else if (event.type === 'thinking') {
|
|
318
|
+
contentStarted = true;
|
|
319
|
+
stream('thinking', { text: event.text });
|
|
320
|
+
}
|
|
321
|
+
else if (event.type === 'toolStart') {
|
|
322
|
+
contentStarted = true;
|
|
323
|
+
this.store.recordToolOrdinal(projectId, attemptId, event.ordinal, event.id, false);
|
|
324
|
+
if (ptcState !== null)
|
|
325
|
+
ptcState.nextOrdinal = Math.max(ptcState.nextOrdinal, event.ordinal + 1);
|
|
326
|
+
stream('tool', { provider_call_id: event.id, capability_id: event.name, phase: 'start', call_ordinal: event.ordinal });
|
|
327
|
+
}
|
|
328
|
+
else if (event.type === 'tool') {
|
|
329
|
+
contentStarted = true;
|
|
330
|
+
this.store.recordToolOrdinal(projectId, attemptId, event.ordinal, event.id, true);
|
|
331
|
+
if (ptcState !== null)
|
|
332
|
+
ptcState.nextOrdinal = Math.max(ptcState.nextOrdinal, event.ordinal + 1);
|
|
333
|
+
stream('tool', { provider_call_id: event.id, capability_id: event.name, phase: 'complete', call_ordinal: event.ordinal });
|
|
334
|
+
if (event.programmaticCaller === true)
|
|
335
|
+
continue;
|
|
336
|
+
if (ptcState !== null && event.name === 'programmatic_tool_run' && event.providerExecuted === true) {
|
|
337
|
+
const argumentsValue = ptcArguments(event.arguments, ptcState.capabilityIds);
|
|
338
|
+
const callId = this.#createPtcRoot(projectId, ptcState, event.ordinal, event.id, argumentsValue);
|
|
339
|
+
toolBlocks.push({
|
|
340
|
+
ordinal: event.ordinal,
|
|
341
|
+
providerId: event.id,
|
|
342
|
+
name: event.name,
|
|
343
|
+
arguments: argumentsValue,
|
|
344
|
+
contextArguments: event.arguments,
|
|
345
|
+
callId
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
toolBlocks.push({ ordinal: event.ordinal, providerId: event.id, name: event.name, arguments: event.arguments });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (event.type === 'toolResult' && ptcState !== null) {
|
|
353
|
+
contentStarted = true;
|
|
354
|
+
this.#completePtcRoot(projectId, ptcState, event.id, event.output);
|
|
355
|
+
}
|
|
356
|
+
else if (event.type === 'usage') {
|
|
357
|
+
usage = { input_tokens: event.inputTokens, output_tokens: event.outputTokens };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ptcState !== null)
|
|
361
|
+
this.#failOpenPtcRoots(projectId, ptcState);
|
|
362
|
+
this.store.transitionAttempt(projectId, attemptId, 'RUNNING', 'TERMINAL', 'success', usage);
|
|
363
|
+
return { attemptId, text, tools: toolBlocks.sort((left, right) => left.ordinal - right.ordinal) };
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
lastError = error;
|
|
367
|
+
if (ptcState !== null)
|
|
368
|
+
this.#failOpenPtcRoots(projectId, ptcState);
|
|
369
|
+
this.store.transitionAttempt(projectId, attemptId, 'RUNNING', 'TERMINAL', signal.aborted ? 'cancelled' : 'failed', usage, code(error));
|
|
370
|
+
if (contentStarted || signal.aborted)
|
|
371
|
+
throw error;
|
|
372
|
+
if (!reactiveCompacted && contextLimit(error)) {
|
|
373
|
+
await this.#compact(projectId, string(agent.id), stepId, candidateContext, configSnapshot, signal);
|
|
374
|
+
const nextModel = this.gateway.resolve(string(agent.model), configSnapshot.config);
|
|
375
|
+
const next = this.context.prepare(projectId, string(agent.id), configSnapshot.config, nextModel.contextWindowTokens, nextModel.profile);
|
|
376
|
+
return this.#sample(projectId, agent, stepId, next, configSnapshot, signal, true);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
throw asError(lastError ?? new Error('All model routes failed.'));
|
|
381
|
+
}
|
|
382
|
+
#modelTools(projectId, agentId) {
|
|
383
|
+
return this.store.listCapabilities(projectId, agentId)
|
|
384
|
+
.filter((entry) => entry.loaded === true)
|
|
385
|
+
.flatMap((entry) => {
|
|
386
|
+
const definition = this.registry.find(string(entry.capability_id));
|
|
387
|
+
if (definition === undefined)
|
|
388
|
+
return [];
|
|
389
|
+
return definition.kind === 'skill' ? null : {
|
|
390
|
+
id: definition.id,
|
|
391
|
+
description: definition.description,
|
|
392
|
+
inputSchema: jsonValue(definition.inputSchema),
|
|
393
|
+
...(definition.outputSchema === undefined ? {} : { outputSchema: jsonValue(definition.outputSchema) })
|
|
394
|
+
};
|
|
395
|
+
})
|
|
396
|
+
.filter((entry) => entry !== null);
|
|
397
|
+
}
|
|
398
|
+
async #materializeCalls(projectId, agent, stepId, sample, signal) {
|
|
399
|
+
const callIds = [];
|
|
400
|
+
let partitionId = null;
|
|
401
|
+
let partitionClaims = [];
|
|
402
|
+
const workspaceId = string(agent.workspace_id);
|
|
403
|
+
const workspacePaths = strings(this.store.getWorkspace(projectId, workspaceId).paths);
|
|
404
|
+
for (const block of sample.tools) {
|
|
405
|
+
if (block.callId !== undefined) {
|
|
406
|
+
callIds.push(block.callId);
|
|
407
|
+
partitionId = null;
|
|
408
|
+
partitionClaims = [];
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
let assignment;
|
|
412
|
+
let claims;
|
|
413
|
+
let effect;
|
|
414
|
+
try {
|
|
415
|
+
assignment = this.store.getCapabilityAssignment(projectId, string(agent.id), block.name);
|
|
416
|
+
if (assignment.loaded !== true)
|
|
417
|
+
fail('CAPABILITY_NOT_LOADED', 403, `Capability ${block.name} is not loaded.`);
|
|
418
|
+
effect = this.registry.effect(block.name, block.arguments);
|
|
419
|
+
claims = this.registry.claims(block.name, { projectId, workspaceId, workspacePaths, arguments: block.arguments });
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
const validationPartition = this.store.createPartition(projectId, stepId, 'serial');
|
|
423
|
+
const call = this.store.createCall(projectId, {
|
|
424
|
+
workspaceId,
|
|
425
|
+
agentId: string(agent.id),
|
|
426
|
+
stepId,
|
|
427
|
+
attemptId: sample.attemptId,
|
|
428
|
+
partitionId: string(validationPartition.id),
|
|
429
|
+
source: 'llm',
|
|
430
|
+
capabilityId: block.name,
|
|
431
|
+
providerCallId: block.providerId,
|
|
432
|
+
callOrdinal: block.ordinal,
|
|
433
|
+
arguments: block.arguments,
|
|
434
|
+
effect: 'full',
|
|
435
|
+
state: 'QUEUED'
|
|
436
|
+
});
|
|
437
|
+
const callId = string(call.id);
|
|
438
|
+
this.store.startCall(projectId, callId);
|
|
439
|
+
const errorCode = code(error);
|
|
440
|
+
this.store.completeCall(projectId, callId, 'failed', { error_code: errorCode }, errorCode, asError(error).message);
|
|
441
|
+
callIds.push(callId);
|
|
442
|
+
partitionId = null;
|
|
443
|
+
partitionClaims = [];
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const conflict = partitionClaims.some((existing) => claimsConflict([existing], claims));
|
|
447
|
+
if (partitionId === null || conflict) {
|
|
448
|
+
const partition = this.store.createPartition(projectId, stepId, conflict ? 'serial' : 'parallel');
|
|
449
|
+
partitionId = string(partition.id);
|
|
450
|
+
partitionClaims = [];
|
|
451
|
+
}
|
|
452
|
+
partitionClaims.push(...claims);
|
|
453
|
+
const policy = policyValue(assignment.policy);
|
|
454
|
+
const decision = decidePolicy(policy, effect);
|
|
455
|
+
let call = this.store.createCall(projectId, {
|
|
456
|
+
workspaceId,
|
|
457
|
+
agentId: string(agent.id),
|
|
458
|
+
stepId,
|
|
459
|
+
attemptId: sample.attemptId,
|
|
460
|
+
partitionId,
|
|
461
|
+
source: 'llm',
|
|
462
|
+
capabilityId: block.name,
|
|
463
|
+
providerCallId: block.providerId,
|
|
464
|
+
callOrdinal: block.ordinal,
|
|
465
|
+
arguments: decision.state === 'WAITING' && !decision.autoReview
|
|
466
|
+
? { arguments: block.arguments, prompt: policyPrompt(block.name) }
|
|
467
|
+
: block.arguments,
|
|
468
|
+
effect,
|
|
469
|
+
policy,
|
|
470
|
+
state: decision.state,
|
|
471
|
+
...(decision.waitReason === undefined ? {} : { waitReason: decision.waitReason })
|
|
472
|
+
});
|
|
473
|
+
const callId = string(call.id);
|
|
474
|
+
callIds.push(callId);
|
|
475
|
+
if (decision.state === 'WAITING' && !decision.autoReview) {
|
|
476
|
+
this.events.emit(projectId, this.store.stateRevision(), 'attention.required', { code: 'CALL_WAITING_USER', entity_type: 'call', entity_id: callId, agent_id: agent.id ?? null });
|
|
477
|
+
}
|
|
478
|
+
if (decision.autoReview) {
|
|
479
|
+
const runId = string(this.store.getStep(projectId, stepId).run_id);
|
|
480
|
+
const goal = string(this.store.getRunGoal(projectId, runId).content);
|
|
481
|
+
const approved = await this.reviewer.review(goal, call, signal).catch(() => false);
|
|
482
|
+
call = this.store.reviewCall(projectId, callId, approved);
|
|
483
|
+
if (!approved)
|
|
484
|
+
this.events.emit(projectId, this.store.stateRevision(), 'attention.required', { code: 'CALL_REVIEW_REQUIRED', entity_type: 'call', entity_id: callId, agent_id: agent.id ?? null });
|
|
485
|
+
}
|
|
486
|
+
if (conflict) {
|
|
487
|
+
partitionId = null;
|
|
488
|
+
partitionClaims = [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return callIds;
|
|
492
|
+
}
|
|
493
|
+
async #executePartitions(projectId, callIds, signal) {
|
|
494
|
+
const partitions = [];
|
|
495
|
+
let currentId = null;
|
|
496
|
+
for (const callId of callIds) {
|
|
497
|
+
const call = this.store.getCall(projectId, callId);
|
|
498
|
+
const partitionId = string(call.partition_id);
|
|
499
|
+
if (partitionId !== currentId) {
|
|
500
|
+
partitions.push([]);
|
|
501
|
+
currentId = partitionId;
|
|
502
|
+
}
|
|
503
|
+
partitions[partitions.length - 1]?.push(callId);
|
|
504
|
+
}
|
|
505
|
+
for (const partition of partitions) {
|
|
506
|
+
for (const callId of partition) {
|
|
507
|
+
if (this.store.getCall(projectId, callId).state === 'QUEUED')
|
|
508
|
+
this.calls.dispatch(projectId, callId, signal);
|
|
509
|
+
}
|
|
510
|
+
await this.calls.waitAll(projectId, partition, signal);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
#createPtcRoot(projectId, state, ordinal, providerCallId, argumentsValue) {
|
|
514
|
+
const definition = this.registry.get('programmatic_tool_run');
|
|
515
|
+
const assignment = this.store.getCapabilityAssignment(projectId, string(state.agent.id), definition.id);
|
|
516
|
+
if (assignment.loaded !== true)
|
|
517
|
+
fail('CAPABILITY_NOT_LOADED', 403, 'Programmatic Tool Calling is not loaded for this Agent.');
|
|
518
|
+
const ptcWorkspaceId = string(state.agent.workspace_id);
|
|
519
|
+
this.registry.claims(definition.id, { projectId, workspaceId: ptcWorkspaceId, workspacePaths: strings(this.store.getWorkspace(projectId, ptcWorkspaceId).paths), arguments: argumentsValue });
|
|
520
|
+
const policy = policyValue(assignment.policy);
|
|
521
|
+
const decision = decidePolicy(policy, definition.effect);
|
|
522
|
+
const partition = this.store.createPartition(projectId, state.stepId, 'serial');
|
|
523
|
+
const call = this.store.createCall(projectId, {
|
|
524
|
+
workspaceId: string(state.agent.workspace_id),
|
|
525
|
+
agentId: string(state.agent.id),
|
|
526
|
+
stepId: state.stepId,
|
|
527
|
+
attemptId: state.attemptId,
|
|
528
|
+
partitionId: string(partition.id),
|
|
529
|
+
source: 'llm',
|
|
530
|
+
capabilityId: definition.id,
|
|
531
|
+
providerCallId,
|
|
532
|
+
callOrdinal: ordinal,
|
|
533
|
+
arguments: argumentsValue,
|
|
534
|
+
effect: definition.effect,
|
|
535
|
+
policy,
|
|
536
|
+
state: 'QUEUED'
|
|
537
|
+
});
|
|
538
|
+
const callId = string(call.id);
|
|
539
|
+
state.providerCalls.set(providerCallId, callId);
|
|
540
|
+
state.rootCallIds.add(callId);
|
|
541
|
+
this.store.startCall(projectId, callId);
|
|
542
|
+
if (decision.state !== 'QUEUED') {
|
|
543
|
+
this.store.completeCall(projectId, callId, 'failed', { error_code: 'POLICY_REQUIRES_DIRECT_CALL' }, 'POLICY_REQUIRES_DIRECT_CALL', 'The hosted program requires a Direct Call.');
|
|
544
|
+
}
|
|
545
|
+
return callId;
|
|
546
|
+
}
|
|
547
|
+
async #executePtcNested(projectId, state, input, signal) {
|
|
548
|
+
if (signal.aborted)
|
|
549
|
+
return { error_code: 'CALL_CANCELLED' };
|
|
550
|
+
if (input.parentProviderCallId === undefined)
|
|
551
|
+
return { error_code: 'POLICY_REQUIRES_DIRECT_CALL' };
|
|
552
|
+
if (input.capabilityId === 'programmatic_tool_run' || input.capabilityId === 'capability_search' || !state.capabilityIds.includes(input.capabilityId)) {
|
|
553
|
+
return { error_code: 'CAPABILITY_NOT_LOADED' };
|
|
554
|
+
}
|
|
555
|
+
const parentCallId = state.providerCalls.get(input.parentProviderCallId);
|
|
556
|
+
if (parentCallId === undefined || !state.rootCallIds.has(parentCallId) || this.store.getCall(projectId, parentCallId).state !== 'RUNNING') {
|
|
557
|
+
return { error_code: 'CALL_STATE_CONFLICT' };
|
|
558
|
+
}
|
|
559
|
+
const existing = state.providerCalls.get(input.providerCallId);
|
|
560
|
+
if (existing !== undefined)
|
|
561
|
+
return this.#ptcResult(projectId, existing, signal);
|
|
562
|
+
if (state.nestedCount >= state.maxToolCalls)
|
|
563
|
+
return { error_code: 'CALL_ARGUMENTS_INVALID', reason: 'PTC tool call limit exceeded.' };
|
|
564
|
+
state.nestedCount += 1;
|
|
565
|
+
const ordinal = Math.max(state.nextOrdinal, input.ordinal);
|
|
566
|
+
state.nextOrdinal = ordinal + 1;
|
|
567
|
+
const partition = this.store.createPartition(projectId, state.stepId, 'serial');
|
|
568
|
+
let definition = null;
|
|
569
|
+
let errorCode = null;
|
|
570
|
+
let effect = 'full';
|
|
571
|
+
let policy;
|
|
572
|
+
try {
|
|
573
|
+
definition = this.registry.get(input.capabilityId);
|
|
574
|
+
const assignment = this.store.getCapabilityAssignment(projectId, string(state.agent.id), input.capabilityId);
|
|
575
|
+
policy = policyValue(assignment.policy);
|
|
576
|
+
if (assignment.loaded !== true)
|
|
577
|
+
fail('CAPABILITY_NOT_LOADED', 403, `Capability ${input.capabilityId} is not loaded.`);
|
|
578
|
+
const hostedWorkspaceId = string(state.agent.workspace_id);
|
|
579
|
+
this.registry.claims(input.capabilityId, { projectId, workspaceId: hostedWorkspaceId, workspacePaths: strings(this.store.getWorkspace(projectId, hostedWorkspaceId).paths), arguments: input.arguments });
|
|
580
|
+
effect = this.registry.effect(input.capabilityId, input.arguments);
|
|
581
|
+
if (decidePolicy(policy, effect).state !== 'QUEUED')
|
|
582
|
+
errorCode = 'POLICY_REQUIRES_DIRECT_CALL';
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
errorCode = code(error);
|
|
586
|
+
}
|
|
587
|
+
const call = this.store.createCall(projectId, {
|
|
588
|
+
workspaceId: string(state.agent.workspace_id),
|
|
589
|
+
agentId: string(state.agent.id),
|
|
590
|
+
stepId: state.stepId,
|
|
591
|
+
attemptId: state.attemptId,
|
|
592
|
+
partitionId: string(partition.id),
|
|
593
|
+
parentCallId,
|
|
594
|
+
source: 'program',
|
|
595
|
+
capabilityId: input.capabilityId,
|
|
596
|
+
providerCallId: input.providerCallId,
|
|
597
|
+
callOrdinal: ordinal,
|
|
598
|
+
arguments: input.arguments,
|
|
599
|
+
effect,
|
|
600
|
+
...(policy === undefined ? {} : { policy }),
|
|
601
|
+
state: 'QUEUED'
|
|
602
|
+
});
|
|
603
|
+
const callId = string(call.id);
|
|
604
|
+
state.providerCalls.set(input.providerCallId, callId);
|
|
605
|
+
if (definition === null || errorCode !== null) {
|
|
606
|
+
this.store.startCall(projectId, callId);
|
|
607
|
+
this.store.completeCall(projectId, callId, 'failed', { error_code: errorCode ?? 'CAPABILITY_NOT_FOUND' }, errorCode ?? 'CAPABILITY_NOT_FOUND');
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
this.calls.dispatch(projectId, callId, signal);
|
|
611
|
+
}
|
|
612
|
+
return this.#ptcResult(projectId, callId, signal);
|
|
613
|
+
}
|
|
614
|
+
async #ptcResult(projectId, callId, signal) {
|
|
615
|
+
await this.calls.waitAll(projectId, [callId], signal);
|
|
616
|
+
const call = this.store.getCall(projectId, callId);
|
|
617
|
+
const result = call.result;
|
|
618
|
+
if (!jsonObject(result))
|
|
619
|
+
return { error_code: 'RESULT_NOT_FOUND' };
|
|
620
|
+
return {
|
|
621
|
+
outcome: result.outcome ?? 'failed',
|
|
622
|
+
content: result.content ?? {},
|
|
623
|
+
error_code: result.error_code ?? null,
|
|
624
|
+
error_message: result.error_message ?? null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
#completePtcRoot(projectId, state, providerCallId, output) {
|
|
628
|
+
const callId = state.providerCalls.get(providerCallId);
|
|
629
|
+
if (callId === undefined || !state.rootCallIds.has(callId))
|
|
630
|
+
return;
|
|
631
|
+
const call = this.store.getCall(projectId, callId);
|
|
632
|
+
if (call.state !== 'RUNNING')
|
|
633
|
+
return;
|
|
634
|
+
const failed = jsonObject(output) && output.status === 'incomplete';
|
|
635
|
+
this.store.completeCall(projectId, callId, failed ? 'failed' : 'success', output, failed ? 'MODEL_STREAM_FAILED' : null);
|
|
636
|
+
}
|
|
637
|
+
#failOpenPtcRoots(projectId, state) {
|
|
638
|
+
for (const callId of state.rootCallIds) {
|
|
639
|
+
let call = this.store.getCall(projectId, callId);
|
|
640
|
+
if (call.state === 'QUEUED')
|
|
641
|
+
call = this.store.startCall(projectId, callId);
|
|
642
|
+
if (call.state === 'RUNNING') {
|
|
643
|
+
this.store.completeCall(projectId, callId, 'failed', { error_code: 'MODEL_STREAM_FAILED' }, 'MODEL_STREAM_FAILED', 'The Provider ended the hosted program without a result.');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async #compact(projectId, agentId, stepId, prepared, configSnapshot, signal, preserveFromOverride) {
|
|
648
|
+
const summary = await this.#summarize(projectId, agentId, stepId, prepared, configSnapshot, signal);
|
|
649
|
+
const compaction = configSnapshot.config.compaction;
|
|
650
|
+
const preserveFrom = preserveFromOverride ?? preservedOrdinal(prepared.source.items, compaction?.preservedSegmentTokens ?? 16000, compaction?.preservedSegmentBytes ?? 262144);
|
|
651
|
+
return this.store.compactContext(projectId, agentId, summary, preserveFrom);
|
|
652
|
+
}
|
|
653
|
+
async #summarize(projectId, agentId, stepId, prepared, configSnapshot, signal) {
|
|
654
|
+
const role = configSnapshot.config.roles.compactor;
|
|
655
|
+
if (role.model === null)
|
|
656
|
+
fail('CONFIG_INVALID', 503, 'The compactor role requires a model.');
|
|
657
|
+
const primary = role.model;
|
|
658
|
+
const candidates = [
|
|
659
|
+
...Array.from({ length: role.providerRouting.retryMaxAttempts }, () => primary),
|
|
660
|
+
...role.providerRouting.fallback
|
|
661
|
+
];
|
|
662
|
+
const source = prepared.messages.map((entry) => `${entry.role}: ${entry.content}`).join('\n\n');
|
|
663
|
+
const compaction = configSnapshot.config.compaction;
|
|
664
|
+
let lastError;
|
|
665
|
+
for (const model of candidates) {
|
|
666
|
+
signal.throwIfAborted();
|
|
667
|
+
const resolved = this.gateway.resolve(model, configSnapshot.config);
|
|
668
|
+
const request = {
|
|
669
|
+
model,
|
|
670
|
+
reasoningEffort: resolveReasoningEffort(resolved.profile, role.modelReasoningEffort),
|
|
671
|
+
maxOutputTokens: compaction?.summaryMaxTokens ?? 8192,
|
|
672
|
+
tools: [],
|
|
673
|
+
signal,
|
|
674
|
+
messages: [
|
|
675
|
+
{ role: 'system', content: resolved.profile.value.systemPrompt.content },
|
|
676
|
+
{ role: 'system', content: 'Summarize the durable facts, decisions, constraints, unfinished work, and tool outcomes. Return only the summary.' },
|
|
677
|
+
{ role: 'user', content: bounded(source, compaction?.preservedSegmentBytes ?? 262144) }
|
|
678
|
+
]
|
|
679
|
+
};
|
|
680
|
+
const attempt = this.store.createAttempt(projectId, {
|
|
681
|
+
agentId,
|
|
682
|
+
stepId,
|
|
683
|
+
kind: 'compaction',
|
|
684
|
+
model,
|
|
685
|
+
profileId: resolved.profile.id,
|
|
686
|
+
profileDigest: resolved.profile.digest,
|
|
687
|
+
profileVersion: resolved.profile.value.version,
|
|
688
|
+
configDigest: configSnapshot.digest,
|
|
689
|
+
contextRevision: prepared.sourceRevision,
|
|
690
|
+
request: frozenRequest(request),
|
|
691
|
+
preparation: {
|
|
692
|
+
context_digest: prepared.digest,
|
|
693
|
+
token_estimate: prepared.estimate,
|
|
694
|
+
projection_decisions: prepared.decisions,
|
|
695
|
+
cache_plan: { status: 'unsupported', hints_applied: false }
|
|
696
|
+
},
|
|
697
|
+
capabilities: []
|
|
698
|
+
});
|
|
699
|
+
const attemptId = string(attempt.id);
|
|
700
|
+
this.store.transitionAttempt(projectId, attemptId, 'PREPARED', 'RUNNING');
|
|
701
|
+
let summary = '';
|
|
702
|
+
try {
|
|
703
|
+
for await (const event of this.gateway.stream(request, configSnapshot.config))
|
|
704
|
+
if (event.type === 'text')
|
|
705
|
+
summary += event.text;
|
|
706
|
+
if (summary.length === 0)
|
|
707
|
+
throw new Error('The Compactor returned an empty summary.');
|
|
708
|
+
this.store.transitionAttempt(projectId, attemptId, 'RUNNING', 'TERMINAL', 'success');
|
|
709
|
+
return summary;
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
lastError = error;
|
|
713
|
+
const current = this.store.getAttempt(projectId, attemptId);
|
|
714
|
+
if (current.state === 'RUNNING')
|
|
715
|
+
this.store.transitionAttempt(projectId, attemptId, 'RUNNING', 'TERMINAL', signal.aborted ? 'cancelled' : 'failed', null, 'COMPACTION_FAILED');
|
|
716
|
+
if (signal.aborted)
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
fail('COMPACTION_FAILED', 503, 'All Compactor routes failed.', { reason: lastError instanceof Error ? lastError.message : String(lastError) });
|
|
721
|
+
}
|
|
722
|
+
#terminateCurrent(projectId, agentId, error) {
|
|
723
|
+
const runs = this.store.listRuns(projectId, agentId, { limit: 1 }).items;
|
|
724
|
+
const run = runs[0];
|
|
725
|
+
if (run === undefined || run.state === 'TERMINAL')
|
|
726
|
+
return;
|
|
727
|
+
const steps = this.store.listSteps(projectId, string(run.id), { limit: 1 }).items;
|
|
728
|
+
const step = steps[0];
|
|
729
|
+
if (step !== undefined && step.state !== 'TERMINAL')
|
|
730
|
+
this.store.finishStep(projectId, string(step.id), 'failed', code(error));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function preserveAfterGroup(items, turnGroupId) {
|
|
734
|
+
const group = items.filter((item) => item.turn_group_id === turnGroupId);
|
|
735
|
+
if (group.length === 0 || group.some((item) => item.locked === true))
|
|
736
|
+
fail('CONTEXT_GROUP_INVALID', 409, 'The Compaction pivot must be an unlocked TurnGroup.');
|
|
737
|
+
return Math.max(...group.map((item) => number(item.ordinal))) + 1;
|
|
738
|
+
}
|
|
739
|
+
function inputLimit(config, contextWindow) {
|
|
740
|
+
return contextWindow - (config.config.contextBudget?.outputReserveTokens ?? 4096) - (config.config.contextBudget?.safetyMarginTokens ?? 1024);
|
|
741
|
+
}
|
|
742
|
+
function frozenRequest(request, ptc = null) {
|
|
743
|
+
return {
|
|
744
|
+
model: request.model,
|
|
745
|
+
messages: request.messages,
|
|
746
|
+
tools: request.tools,
|
|
747
|
+
reasoning_effort: request.reasoningEffort,
|
|
748
|
+
max_output_tokens: request.maxOutputTokens ?? null,
|
|
749
|
+
programmatic_tool_calling: ptc === null ? null : {
|
|
750
|
+
enabled: true,
|
|
751
|
+
capability_ids: ptc.capabilityIds,
|
|
752
|
+
max_tool_calls: ptc.maxToolCalls,
|
|
753
|
+
schema_version: ptc.schemaVersion,
|
|
754
|
+
instruction: ptc.instruction
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function preservedOrdinal(items, tokenLimit, byteLimit) {
|
|
759
|
+
let tokens = 0;
|
|
760
|
+
let bytes = 0;
|
|
761
|
+
let preserveFrom = items.length;
|
|
762
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
763
|
+
const item = items[index];
|
|
764
|
+
if (item === undefined)
|
|
765
|
+
continue;
|
|
766
|
+
const content = string(item.content);
|
|
767
|
+
const nextTokens = tokens + estimateTokens(content).safe;
|
|
768
|
+
const nextBytes = bytes + Buffer.byteLength(content);
|
|
769
|
+
if (nextTokens > tokenLimit || nextBytes > byteLimit)
|
|
770
|
+
break;
|
|
771
|
+
tokens = nextTokens;
|
|
772
|
+
bytes = nextBytes;
|
|
773
|
+
preserveFrom = number(item.ordinal);
|
|
774
|
+
}
|
|
775
|
+
return preserveFrom;
|
|
776
|
+
}
|
|
777
|
+
function policyValue(value) {
|
|
778
|
+
if (value === 'read_only' || value === 'workspace_write' || value === 'external' || value === 'ask' || value === 'auto_approve' || value === 'full_access')
|
|
779
|
+
return value;
|
|
780
|
+
fail('DATABASE_CORRUPT', 503, 'Capability policy is invalid.');
|
|
781
|
+
}
|
|
782
|
+
function ptcArguments(value, capabilityIds) {
|
|
783
|
+
if (!jsonObject(value) || typeof value.code !== 'string' || typeof value.fingerprint !== 'string') {
|
|
784
|
+
fail('ADAPTER_PROTOCOL_ERROR', 503, 'The Provider returned an invalid Programmatic Tool Call.');
|
|
785
|
+
}
|
|
786
|
+
return { program: value.code, capabilityIds: [...capabilityIds] };
|
|
787
|
+
}
|
|
788
|
+
function string(value) {
|
|
789
|
+
if (typeof value !== 'string')
|
|
790
|
+
fail('DATABASE_CORRUPT', 503, 'A runtime field is invalid.');
|
|
791
|
+
return value;
|
|
792
|
+
}
|
|
793
|
+
function number(value) {
|
|
794
|
+
if (typeof value !== 'number')
|
|
795
|
+
fail('DATABASE_CORRUPT', 503, 'A numeric runtime field is invalid.');
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
function strings(value) {
|
|
799
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string'))
|
|
800
|
+
fail('DATABASE_CORRUPT', 503, 'Workspace paths are invalid.');
|
|
801
|
+
return value;
|
|
802
|
+
}
|
|
803
|
+
function bounded(value, maximum) {
|
|
804
|
+
const bytes = Buffer.from(value);
|
|
805
|
+
return bytes.byteLength <= maximum ? value : bytes.subarray(0, maximum).toString();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* The documented error code for a failure. `RUNTIME_FAILED` is the catch-all and is not in 07's taxonomy;
|
|
809
|
+
* it was answering for a missing API key, an untrusted TLS chain, the wrong auth header, an unsupported
|
|
810
|
+
* reasoning effort and a Provider-rejected Tool block alike, so the one thing the reader could see said
|
|
811
|
+
* nothing about any of them.
|
|
812
|
+
*
|
|
813
|
+
* An AI SDK error carries its kind on `name` rather than `code`, which is why it fell through to the
|
|
814
|
+
* catch-all. A rejected request is `MODEL_STREAM_FAILED` (07: Provider stream failed, 502).
|
|
815
|
+
*/
|
|
816
|
+
function code(error) {
|
|
817
|
+
if (typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string')
|
|
818
|
+
return error.code;
|
|
819
|
+
if (isProviderError(error))
|
|
820
|
+
return 'MODEL_STREAM_FAILED';
|
|
821
|
+
return 'RUNTIME_FAILED';
|
|
822
|
+
}
|
|
823
|
+
function isProviderError(error) {
|
|
824
|
+
if (typeof error !== 'object' || error === null)
|
|
825
|
+
return false;
|
|
826
|
+
const name = 'name' in error && typeof error.name === 'string' ? error.name : '';
|
|
827
|
+
return name.startsWith('AI_');
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* What the failure said, for `RunDetail.error.message`. The Provider's own text is the part that makes a
|
|
831
|
+
* failure actionable -- "The format of the value at messages.27.content.0.toolUse.input is invalid" names
|
|
832
|
+
* the fault, where `RUNTIME_FAILED` named nothing.
|
|
833
|
+
*
|
|
834
|
+
* Bounded, and never the whole error object: an AI SDK error can carry the entire request body, which
|
|
835
|
+
* includes the Context and could include an API key from the request headers.
|
|
836
|
+
*/
|
|
837
|
+
function reason(error) {
|
|
838
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : null;
|
|
839
|
+
if (message === null)
|
|
840
|
+
return null;
|
|
841
|
+
const trimmed = message.trim();
|
|
842
|
+
return trimmed.length === 0 ? null : trimmed.slice(0, 500);
|
|
843
|
+
}
|
|
844
|
+
function asError(value) {
|
|
845
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
846
|
+
}
|
|
847
|
+
function contextLimit(error) {
|
|
848
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
849
|
+
return /context.{0,20}(?:length|limit|window)|too many tokens|input.{0,20}too long/i.test(message);
|
|
850
|
+
}
|
|
851
|
+
function jsonObject(value) {
|
|
852
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
853
|
+
}
|