dot-studio 0.0.1
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 +214 -0
- package/client/assets/index-C2eIILoa.css +41 -0
- package/client/assets/index-DUPZ_Lw5.js +616 -0
- package/client/assets/index.es-Btlrnc3g.js +1 -0
- package/client/index.html +14 -0
- package/dist/cli.js +196 -0
- package/dist/server/index.js +79 -0
- package/dist/server/lib/act-runtime.js +1282 -0
- package/dist/server/lib/cache.js +31 -0
- package/dist/server/lib/config.js +53 -0
- package/dist/server/lib/dot-authoring.js +245 -0
- package/dist/server/lib/dot-loader.js +61 -0
- package/dist/server/lib/dot-login.js +190 -0
- package/dist/server/lib/model-catalog.js +111 -0
- package/dist/server/lib/opencode-auth.js +69 -0
- package/dist/server/lib/opencode-errors.js +220 -0
- package/dist/server/lib/opencode-sidecar.js +144 -0
- package/dist/server/lib/opencode.js +12 -0
- package/dist/server/lib/package-bin.js +63 -0
- package/dist/server/lib/project-config.js +39 -0
- package/dist/server/lib/prompt.js +222 -0
- package/dist/server/lib/request-context.js +27 -0
- package/dist/server/lib/runtime-tools.js +208 -0
- package/dist/server/routes/assets.js +161 -0
- package/dist/server/routes/chat.js +356 -0
- package/dist/server/routes/compile.js +105 -0
- package/dist/server/routes/dot.js +270 -0
- package/dist/server/routes/health.js +56 -0
- package/dist/server/routes/opencode.js +421 -0
- package/dist/server/routes/stages.js +137 -0
- package/dist/server/start.js +23 -0
- package/dist/server/terminal.js +282 -0
- package/dist/shared/mcp-config.js +19 -0
- package/dist/shared/model-variants.js +50 -0
- package/dist/shared/project-mcp.js +22 -0
- package/dist/shared/session-metadata.js +26 -0
- package/package.json +103 -0
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
import { createActor, fromPromise, setup, toPromise, assign } from 'xstate';
|
|
2
|
+
import { readAsset } from 'dance-of-tal/lib/registry';
|
|
3
|
+
import { getOpencode } from './opencode.js';
|
|
4
|
+
import { buildPromptEnvelope } from './prompt.js';
|
|
5
|
+
import { buildEnabledToolMap, describeUnavailableRuntimeTools, resolveRuntimeTools } from './runtime-tools.js';
|
|
6
|
+
import { normalizeOpencodeError, unwrapOpencodeResult, unwrapPromptResult } from './opencode-errors.js';
|
|
7
|
+
function makeId(prefix) {
|
|
8
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
9
|
+
}
|
|
10
|
+
function runtimeAssetRefKey(ref) {
|
|
11
|
+
if (!ref) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return ref.kind === 'registry' ? `registry:${ref.urn}` : `draft:${ref.draftId}`;
|
|
15
|
+
}
|
|
16
|
+
function normalizeActSessionMode(mode) {
|
|
17
|
+
return mode === 'default' ? 'default' : 'all_nodes_thread';
|
|
18
|
+
}
|
|
19
|
+
function resolveNodeSessionSettings(act, node) {
|
|
20
|
+
if (node.sessionModeOverride || normalizeActSessionMode(act.sessionMode) === 'default') {
|
|
21
|
+
return {
|
|
22
|
+
policy: node.sessionPolicy,
|
|
23
|
+
lifetime: node.sessionLifetime,
|
|
24
|
+
inheritedFromAct: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
policy: 'node',
|
|
29
|
+
lifetime: 'thread',
|
|
30
|
+
inheritedFromAct: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function buildNodeRuntimeConfigKey(act, node, performer, modelVariant) {
|
|
34
|
+
const agentId = performer.agentId || (performer.planMode ? 'plan' : 'build');
|
|
35
|
+
const session = resolveNodeSessionSettings(act, node);
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
nodeType: node.type,
|
|
38
|
+
performerId: node.performerId || null,
|
|
39
|
+
talRef: runtimeAssetRefKey(performer.talRef),
|
|
40
|
+
danceRefs: performer.danceRefs.map((ref) => runtimeAssetRefKey(ref)).filter(Boolean).sort(),
|
|
41
|
+
model: performer.model
|
|
42
|
+
? { provider: performer.model.provider, modelId: performer.model.modelId }
|
|
43
|
+
: null,
|
|
44
|
+
agentId,
|
|
45
|
+
modelVariant: modelVariant || null,
|
|
46
|
+
mcpServerNames: [...performer.mcpServerNames].sort(),
|
|
47
|
+
danceDeliveryMode: performer.danceDeliveryMode,
|
|
48
|
+
actSessionMode: normalizeActSessionMode(act.sessionMode),
|
|
49
|
+
sessionPolicy: session.policy,
|
|
50
|
+
sessionLifetime: session.lifetime,
|
|
51
|
+
sessionModeOverride: !!node.sessionModeOverride,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const ACT_THREAD_RUNTIME_TTL_MS = 1000 * 60 * 60 * 6;
|
|
55
|
+
const MAX_THREAD_RUNTIME_RECORDS = 200;
|
|
56
|
+
const actThreadRuntimeCache = new Map();
|
|
57
|
+
const actRuntimeBindingCache = new Map();
|
|
58
|
+
const actRuntimeSubscribers = new Map();
|
|
59
|
+
const actRuntimeAbortRequests = new Map();
|
|
60
|
+
class ActRuntimeInterruptedError extends Error {
|
|
61
|
+
constructor(message = 'Act run interrupted.') {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'ActRuntimeInterruptedError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isActRuntimeInterrupted(error) {
|
|
67
|
+
return error instanceof ActRuntimeInterruptedError;
|
|
68
|
+
}
|
|
69
|
+
function isAbortRequested(actSessionId) {
|
|
70
|
+
return !!(actSessionId && actRuntimeAbortRequests.has(actSessionId));
|
|
71
|
+
}
|
|
72
|
+
function assertActNotAborted(context) {
|
|
73
|
+
if (isAbortRequested(context.actSessionId)) {
|
|
74
|
+
throw new ActRuntimeInterruptedError();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function buildResumeSummaryFromContext(context) {
|
|
78
|
+
return {
|
|
79
|
+
updatedAt: Date.now(),
|
|
80
|
+
runId: context.runId || null,
|
|
81
|
+
currentNodeId: context.currentNodeId,
|
|
82
|
+
finalOutput: context.finalOutput,
|
|
83
|
+
error: context.error,
|
|
84
|
+
iterations: context.iterations,
|
|
85
|
+
nodeOutputs: Object.fromEntries(Object.entries(context.nodeOutputs || {})
|
|
86
|
+
.filter((entry) => typeof entry[1] === 'string')),
|
|
87
|
+
history: [...context.history],
|
|
88
|
+
sessionHandles: Array.from(context.threadSessionHandles.values()).map((session) => ({
|
|
89
|
+
handle: session.handle,
|
|
90
|
+
nodeId: session.nodeId,
|
|
91
|
+
nodeType: session.nodeType,
|
|
92
|
+
performerId: session.performerId,
|
|
93
|
+
status: session.status,
|
|
94
|
+
turnCount: session.turnCount,
|
|
95
|
+
lastUsedAt: session.lastUsedAt,
|
|
96
|
+
summary: session.summary,
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function emitActRuntimeProgress(context, status) {
|
|
101
|
+
if (!context.actSessionId) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const listeners = actRuntimeSubscribers.get(context.actSessionId);
|
|
105
|
+
if (!listeners || listeners.size === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const event = {
|
|
109
|
+
type: 'act.runtime',
|
|
110
|
+
actSessionId: context.actSessionId,
|
|
111
|
+
actId: context.act.id,
|
|
112
|
+
runId: context.runId,
|
|
113
|
+
status,
|
|
114
|
+
summary: buildResumeSummaryFromContext(context),
|
|
115
|
+
};
|
|
116
|
+
for (const listener of listeners) {
|
|
117
|
+
try {
|
|
118
|
+
listener(event);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Ignore subscriber failures and keep the runtime moving.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function emitActPerformerBinding(context, sessionId, node, performer) {
|
|
126
|
+
if (!context.actSessionId) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const nodeLabel = typeof node.label === 'string' ? node.label : '';
|
|
130
|
+
const event = {
|
|
131
|
+
type: 'act.performer.binding',
|
|
132
|
+
actSessionId: context.actSessionId,
|
|
133
|
+
actId: context.act.id,
|
|
134
|
+
runId: context.runId,
|
|
135
|
+
sessionId,
|
|
136
|
+
nodeId: node.id,
|
|
137
|
+
nodeLabel: nodeLabel || performer.name || node.id,
|
|
138
|
+
performerId: performer.id || null,
|
|
139
|
+
performerName: performer.name || null,
|
|
140
|
+
};
|
|
141
|
+
const cached = actRuntimeBindingCache.get(context.actSessionId) || {
|
|
142
|
+
actId: context.act.id,
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
bindings: new Map(),
|
|
145
|
+
};
|
|
146
|
+
cached.updatedAt = Date.now();
|
|
147
|
+
cached.actId = context.act.id;
|
|
148
|
+
cached.bindings.set(sessionId, event);
|
|
149
|
+
actRuntimeBindingCache.set(context.actSessionId, cached);
|
|
150
|
+
const listeners = actRuntimeSubscribers.get(context.actSessionId);
|
|
151
|
+
if (!listeners || listeners.size === 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const listener of listeners) {
|
|
155
|
+
try {
|
|
156
|
+
listener(event);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Ignore subscriber failures and keep the runtime moving.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function subscribeActRuntimeEvents(actSessionId, listener) {
|
|
164
|
+
const listeners = actRuntimeSubscribers.get(actSessionId) || new Set();
|
|
165
|
+
listeners.add(listener);
|
|
166
|
+
actRuntimeSubscribers.set(actSessionId, listeners);
|
|
167
|
+
const cachedBindings = actRuntimeBindingCache.get(actSessionId);
|
|
168
|
+
if (cachedBindings) {
|
|
169
|
+
for (const event of cachedBindings.bindings.values()) {
|
|
170
|
+
try {
|
|
171
|
+
listener(event);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Ignore subscriber failures during replay.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return () => {
|
|
179
|
+
const current = actRuntimeSubscribers.get(actSessionId);
|
|
180
|
+
if (!current) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
current.delete(listener);
|
|
184
|
+
if (current.size === 0) {
|
|
185
|
+
actRuntimeSubscribers.delete(actSessionId);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export async function abortActRuntime(actSessionId, cwd) {
|
|
190
|
+
actRuntimeAbortRequests.set(actSessionId, Date.now());
|
|
191
|
+
const cachedBindings = actRuntimeBindingCache.get(actSessionId);
|
|
192
|
+
const sessionIds = Array.from(new Set(Array.from(cachedBindings?.bindings.values() || []).map((binding) => binding.sessionId).filter(Boolean)));
|
|
193
|
+
if (sessionIds.length === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const oc = await getOpencode();
|
|
197
|
+
await Promise.all(sessionIds.map(async (sessionId) => {
|
|
198
|
+
try {
|
|
199
|
+
await oc.session.abort({
|
|
200
|
+
sessionID: sessionId,
|
|
201
|
+
directory: cwd,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Ignore abort errors; the runtime interrupt flag is the primary stop signal.
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
function extractTextFromResponse(result) {
|
|
210
|
+
const record = result;
|
|
211
|
+
const structured = record?.data?.info?.structured ?? record?.info?.structured ?? record?.structured;
|
|
212
|
+
if (structured && typeof structured === 'object') {
|
|
213
|
+
return JSON.stringify(structured);
|
|
214
|
+
}
|
|
215
|
+
const parts = [
|
|
216
|
+
...(record?.parts || []),
|
|
217
|
+
...(record?.data?.parts || []),
|
|
218
|
+
...(record?.info?.parts || []),
|
|
219
|
+
];
|
|
220
|
+
const text = parts
|
|
221
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
222
|
+
.map((part) => part.text)
|
|
223
|
+
.join('\n')
|
|
224
|
+
.trim();
|
|
225
|
+
if (text) {
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
if (typeof record?.text === 'string' && record.text.trim()) {
|
|
229
|
+
return record.text.trim();
|
|
230
|
+
}
|
|
231
|
+
return JSON.stringify(result);
|
|
232
|
+
}
|
|
233
|
+
function extractJsonObject(text) {
|
|
234
|
+
const fencedMatch = text.match(/```json\s*([\s\S]*?)```/i) || text.match(/```\s*([\s\S]*?)```/i);
|
|
235
|
+
if (fencedMatch?.[1]) {
|
|
236
|
+
return fencedMatch[1].trim();
|
|
237
|
+
}
|
|
238
|
+
const start = text.indexOf('{');
|
|
239
|
+
const end = text.lastIndexOf('}');
|
|
240
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
241
|
+
throw new Error('Expected JSON object in orchestrator response.');
|
|
242
|
+
}
|
|
243
|
+
return text.slice(start, end + 1);
|
|
244
|
+
}
|
|
245
|
+
function parseOrchestratorDecision(text, routes) {
|
|
246
|
+
const parsed = JSON.parse(extractJsonObject(text));
|
|
247
|
+
const next = typeof parsed.next === 'string' ? parsed.next : '';
|
|
248
|
+
const input = typeof parsed.input === 'string' ? parsed.input : '';
|
|
249
|
+
const allowedRoutes = new Set([...routes, '$exit']);
|
|
250
|
+
if (!allowedRoutes.has(next)) {
|
|
251
|
+
throw new Error(`Orchestrator chose invalid route '${next}'. Allowed routes: ${Array.from(allowedRoutes).join(', ')}`);
|
|
252
|
+
}
|
|
253
|
+
const sessionMode = typeof parsed.session?.mode === 'string' ? parsed.session.mode : undefined;
|
|
254
|
+
const sessionHandle = typeof parsed.session?.handle === 'string' ? parsed.session.handle : undefined;
|
|
255
|
+
const session = sessionMode === 'fresh'
|
|
256
|
+
? { mode: 'fresh' }
|
|
257
|
+
: sessionMode === 'reuse'
|
|
258
|
+
? { mode: 'reuse', ...(sessionHandle ? { handle: sessionHandle } : {}) }
|
|
259
|
+
: undefined;
|
|
260
|
+
return { next, input, ...(session ? { session } : {}) };
|
|
261
|
+
}
|
|
262
|
+
function buildOrchestratorFormat(routes) {
|
|
263
|
+
return {
|
|
264
|
+
type: 'json_schema',
|
|
265
|
+
retryCount: 1,
|
|
266
|
+
schema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
required: ['next', 'input'],
|
|
270
|
+
properties: {
|
|
271
|
+
next: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
enum: [...routes, '$exit'],
|
|
274
|
+
},
|
|
275
|
+
input: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
},
|
|
278
|
+
session: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
required: ['mode'],
|
|
282
|
+
properties: {
|
|
283
|
+
mode: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
enum: ['fresh', 'reuse'],
|
|
286
|
+
},
|
|
287
|
+
handle: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function buildOrchestratorPrompt(input, routes, availableSessions) {
|
|
297
|
+
const sessionCatalog = availableSessions.length > 0
|
|
298
|
+
? [
|
|
299
|
+
'',
|
|
300
|
+
'Available reusable session handles for this thread:',
|
|
301
|
+
...availableSessions.map((session) => (`- handle=${session.handle}; node=${session.nodeId}; type=${session.nodeType}; turns=${session.turnCount}; lastUsedAt=${new Date(session.lastUsedAt).toISOString()}; summary=${session.summary || ''}`)),
|
|
302
|
+
'If the next node should continue prior memory, respond with session.mode="reuse" and one of the handles for that node.',
|
|
303
|
+
'If the next node should start fresh, respond with session.mode="fresh".',
|
|
304
|
+
].join('\n')
|
|
305
|
+
: '\nNo reusable session handles are currently available for the allowed routes. Use session.mode="fresh" if you include a session field.';
|
|
306
|
+
return [
|
|
307
|
+
input,
|
|
308
|
+
'',
|
|
309
|
+
'You are an orchestrator. Your role is to read the input above and decide which node should handle it next.',
|
|
310
|
+
'Choose the next route.',
|
|
311
|
+
`Allowed next values: ${[...routes, '$exit'].join(', ')}`,
|
|
312
|
+
sessionCatalog,
|
|
313
|
+
'Respond with JSON only in this exact shape:',
|
|
314
|
+
'{"next":"<nodeId|$exit>","input":"<string>","session":{"mode":"fresh"|"reuse","handle":"<handle when reusing>"}}',
|
|
315
|
+
].join('\n');
|
|
316
|
+
}
|
|
317
|
+
function getOutgoingEdges(act, nodeId) {
|
|
318
|
+
return act.edges.filter((edge) => edge.from === nodeId);
|
|
319
|
+
}
|
|
320
|
+
function getOrchestratorRoutes(act, nodeId) {
|
|
321
|
+
return getOutgoingEdges(act, nodeId)
|
|
322
|
+
.filter((edge) => edge.role !== 'branch')
|
|
323
|
+
.map((edge) => edge.to);
|
|
324
|
+
}
|
|
325
|
+
function getParallelBranches(act, nodeId) {
|
|
326
|
+
return getOutgoingEdges(act, nodeId)
|
|
327
|
+
.filter((edge) => edge.role === 'branch' && edge.to !== '$exit')
|
|
328
|
+
.map((edge) => edge.to);
|
|
329
|
+
}
|
|
330
|
+
function selectNextTarget(act, nodeId, outcome) {
|
|
331
|
+
const edges = getOutgoingEdges(act, nodeId).filter((edge) => edge.role !== 'branch');
|
|
332
|
+
const preferredConditions = outcome === 'success'
|
|
333
|
+
? ['on_success', 'always', undefined]
|
|
334
|
+
: ['on_fail', 'always', undefined];
|
|
335
|
+
for (const condition of preferredConditions) {
|
|
336
|
+
const match = edges.find((edge) => edge.condition === condition || (!edge.condition && condition === undefined));
|
|
337
|
+
if (match) {
|
|
338
|
+
return match.to;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
function cloneContext(context) {
|
|
344
|
+
return {
|
|
345
|
+
...context,
|
|
346
|
+
history: [...context.history],
|
|
347
|
+
sharedState: { ...context.sharedState },
|
|
348
|
+
nodeOutputs: { ...context.nodeOutputs },
|
|
349
|
+
sessionPool: new Map(context.sessionPool),
|
|
350
|
+
threadSessionHandles: new Map(context.threadSessionHandles),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function pruneActThreadRuntimeCache() {
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
for (const [sessionId, record] of actThreadRuntimeCache.entries()) {
|
|
356
|
+
if (now - record.updatedAt > ACT_THREAD_RUNTIME_TTL_MS) {
|
|
357
|
+
actThreadRuntimeCache.delete(sessionId);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const [sessionId, record] of actRuntimeBindingCache.entries()) {
|
|
361
|
+
if (now - record.updatedAt > ACT_THREAD_RUNTIME_TTL_MS) {
|
|
362
|
+
actRuntimeBindingCache.delete(sessionId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (actThreadRuntimeCache.size <= MAX_THREAD_RUNTIME_RECORDS) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const oldestEntries = [...actThreadRuntimeCache.entries()]
|
|
369
|
+
.sort((a, b) => a[1].updatedAt - b[1].updatedAt)
|
|
370
|
+
.slice(0, actThreadRuntimeCache.size - MAX_THREAD_RUNTIME_RECORDS);
|
|
371
|
+
for (const [sessionId] of oldestEntries) {
|
|
372
|
+
actThreadRuntimeCache.delete(sessionId);
|
|
373
|
+
actRuntimeBindingCache.delete(sessionId);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function getThreadRuntimeHandles(actSessionId, actId) {
|
|
377
|
+
pruneActThreadRuntimeCache();
|
|
378
|
+
if (!actSessionId) {
|
|
379
|
+
return new Map();
|
|
380
|
+
}
|
|
381
|
+
const existing = actThreadRuntimeCache.get(actSessionId);
|
|
382
|
+
if (existing) {
|
|
383
|
+
existing.updatedAt = Date.now();
|
|
384
|
+
existing.actId = actId;
|
|
385
|
+
return new Map(existing.handles);
|
|
386
|
+
}
|
|
387
|
+
const next = {
|
|
388
|
+
actId,
|
|
389
|
+
updatedAt: Date.now(),
|
|
390
|
+
handles: new Map(),
|
|
391
|
+
};
|
|
392
|
+
actThreadRuntimeCache.set(actSessionId, next);
|
|
393
|
+
return new Map(next.handles);
|
|
394
|
+
}
|
|
395
|
+
function persistThreadRuntimeHandles(actSessionId, actId, handles) {
|
|
396
|
+
if (!actSessionId) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
actThreadRuntimeCache.set(actSessionId, {
|
|
400
|
+
actId,
|
|
401
|
+
updatedAt: Date.now(),
|
|
402
|
+
handles: new Map(handles),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function summarizeText(text) {
|
|
406
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, 180);
|
|
407
|
+
}
|
|
408
|
+
function describeActRuntimeError(error, context) {
|
|
409
|
+
if (error instanceof Error && error.message.trim()) {
|
|
410
|
+
return error.message;
|
|
411
|
+
}
|
|
412
|
+
const normalized = normalizeOpencodeError(error, context?.model ? { model: context.model } : {});
|
|
413
|
+
return normalized.error || normalized.detail || 'OpenCode request failed.';
|
|
414
|
+
}
|
|
415
|
+
function buildColdStartResumeLines(summary) {
|
|
416
|
+
if (!summary) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const lines = [
|
|
420
|
+
'This act thread was restored after a runtime restart. No live reusable node sessions are currently attached.',
|
|
421
|
+
'Use the following as historical thread context only. Do not assume these previous handles are still reusable unless new live handles are listed separately.',
|
|
422
|
+
];
|
|
423
|
+
if (summary.finalOutput) {
|
|
424
|
+
lines.push(`Previous final output: ${summarizeText(summary.finalOutput)}`);
|
|
425
|
+
}
|
|
426
|
+
if (summary.error) {
|
|
427
|
+
lines.push(`Previous error: ${summarizeText(summary.error)}`);
|
|
428
|
+
}
|
|
429
|
+
if (summary.currentNodeId) {
|
|
430
|
+
lines.push(`Previous current node: ${summary.currentNodeId}`);
|
|
431
|
+
}
|
|
432
|
+
if (typeof summary.iterations === 'number') {
|
|
433
|
+
lines.push(`Previous iterations: ${summary.iterations}`);
|
|
434
|
+
}
|
|
435
|
+
const nodeOutputs = Object.entries(summary.nodeOutputs || {})
|
|
436
|
+
.filter(([, value]) => typeof value === 'string' && value.trim())
|
|
437
|
+
.slice(0, 6);
|
|
438
|
+
if (nodeOutputs.length > 0) {
|
|
439
|
+
lines.push('Previous node outputs:');
|
|
440
|
+
for (const [nodeId, value] of nodeOutputs) {
|
|
441
|
+
lines.push(`- ${nodeId}: ${summarizeText(value)}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const history = (summary.history || []).slice(-8);
|
|
445
|
+
if (history.length > 0) {
|
|
446
|
+
lines.push('Recent act history:');
|
|
447
|
+
for (const entry of history) {
|
|
448
|
+
lines.push(`- ${entry.nodeId} (${entry.nodeType}): ${entry.action}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const sessionHandles = (summary.sessionHandles || []).slice(0, 6);
|
|
452
|
+
if (sessionHandles.length > 0) {
|
|
453
|
+
lines.push('Previously warm thread handles (historical only):');
|
|
454
|
+
for (const session of sessionHandles) {
|
|
455
|
+
lines.push(`- ${session.handle}; node=${session.nodeId}; type=${session.nodeType}; turns=${session.turnCount}; summary=${session.summary || ''}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return lines;
|
|
459
|
+
}
|
|
460
|
+
function buildActRuntimeSystem(context, node) {
|
|
461
|
+
const lines = [
|
|
462
|
+
'# Runtime Context',
|
|
463
|
+
`Workflow: ${context.act.name}`,
|
|
464
|
+
`Node: ${node.id} (${node.type})`,
|
|
465
|
+
`Turn input: ${summarizeText(context.pendingInput)}`,
|
|
466
|
+
];
|
|
467
|
+
if (context.actSessionId && context.threadSessionHandles.size === 0) {
|
|
468
|
+
lines.push(...buildColdStartResumeLines(context.resumeSummary));
|
|
469
|
+
}
|
|
470
|
+
return lines.join('\n');
|
|
471
|
+
}
|
|
472
|
+
function buildPersistentHandle(act, lifetime, policy, nodeId, performerId) {
|
|
473
|
+
if (lifetime !== 'thread' || policy === 'fresh') {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
if (policy === 'node') {
|
|
477
|
+
return `node:${nodeId}:thread`;
|
|
478
|
+
}
|
|
479
|
+
if (policy === 'performer') {
|
|
480
|
+
return `performer:${performerId || 'unassigned'}:thread`;
|
|
481
|
+
}
|
|
482
|
+
return `act:${act.id}:thread`;
|
|
483
|
+
}
|
|
484
|
+
function listAvailableSessionHandles(context, routes) {
|
|
485
|
+
const allowedNodeIds = new Set(routes.filter((route) => route !== '$exit'));
|
|
486
|
+
return Array.from(context.threadSessionHandles.values())
|
|
487
|
+
.filter((handle) => allowedNodeIds.has(handle.nodeId))
|
|
488
|
+
.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
489
|
+
}
|
|
490
|
+
function stageActFromRegistryAct(act) {
|
|
491
|
+
const performerIdByUrn = new Map();
|
|
492
|
+
const performers = [];
|
|
493
|
+
for (const node of Object.values(act.nodes || {})) {
|
|
494
|
+
if ((node.type === 'worker' || node.type === 'orchestrator') && typeof node.performer === 'string') {
|
|
495
|
+
const performerUrn = node.performer.trim();
|
|
496
|
+
if (!performerIdByUrn.has(performerUrn)) {
|
|
497
|
+
const id = makeId('performer');
|
|
498
|
+
performerIdByUrn.set(performerUrn, id);
|
|
499
|
+
performers.push({
|
|
500
|
+
id,
|
|
501
|
+
name: performerUrn.split('/').pop() || 'Performer',
|
|
502
|
+
meta: { derivedFrom: performerUrn },
|
|
503
|
+
agentId: null,
|
|
504
|
+
modelVariant: null,
|
|
505
|
+
danceDeliveryMode: 'auto',
|
|
506
|
+
planMode: false,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const stageAct = {
|
|
512
|
+
id: makeId('act'),
|
|
513
|
+
name: act.name,
|
|
514
|
+
description: act.description,
|
|
515
|
+
sessionMode: 'all_nodes_thread',
|
|
516
|
+
meta: act.type.startsWith('act/') ? { derivedFrom: act.type } : undefined,
|
|
517
|
+
entryNodeId: act.entryNode,
|
|
518
|
+
nodes: Object.entries(act.nodes || {}).map(([id, node]) => {
|
|
519
|
+
if (node.type === 'parallel') {
|
|
520
|
+
return {
|
|
521
|
+
id,
|
|
522
|
+
type: 'parallel',
|
|
523
|
+
position: { x: 28, y: 56 },
|
|
524
|
+
join: node.join || 'all',
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (node.type === 'orchestrator') {
|
|
528
|
+
return {
|
|
529
|
+
id,
|
|
530
|
+
type: 'orchestrator',
|
|
531
|
+
performerId: performerIdByUrn.get(node.performer) || null,
|
|
532
|
+
modelVariant: null,
|
|
533
|
+
position: { x: 28, y: 56 },
|
|
534
|
+
maxDelegations: node.maxDelegations,
|
|
535
|
+
sessionPolicy: 'node',
|
|
536
|
+
sessionLifetime: 'thread',
|
|
537
|
+
sessionModeOverride: false,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
id,
|
|
542
|
+
type: 'worker',
|
|
543
|
+
performerId: performerIdByUrn.get(node.performer) || null,
|
|
544
|
+
modelVariant: null,
|
|
545
|
+
position: { x: 28, y: 56 },
|
|
546
|
+
sessionPolicy: 'fresh',
|
|
547
|
+
sessionLifetime: 'run',
|
|
548
|
+
sessionModeOverride: false,
|
|
549
|
+
};
|
|
550
|
+
}),
|
|
551
|
+
edges: (act.edges || []).map((edge, index) => ({
|
|
552
|
+
id: `edge-${index + 1}`,
|
|
553
|
+
from: edge.from,
|
|
554
|
+
to: edge.to,
|
|
555
|
+
...((edge.role === 'branch') ? { role: 'branch' } : {}),
|
|
556
|
+
condition: edge.condition,
|
|
557
|
+
})),
|
|
558
|
+
maxIterations: act.maxIterations || 10,
|
|
559
|
+
};
|
|
560
|
+
return { stageAct, performers };
|
|
561
|
+
}
|
|
562
|
+
async function loadActDefinition(cwd, actUrn) {
|
|
563
|
+
if (!actUrn.startsWith('act/')) {
|
|
564
|
+
throw new Error(`Act URN must start with 'act/': ${actUrn}`);
|
|
565
|
+
}
|
|
566
|
+
const act = await readAsset(cwd, actUrn);
|
|
567
|
+
if (!act) {
|
|
568
|
+
throw new Error(`Act asset not found: ${actUrn}`);
|
|
569
|
+
}
|
|
570
|
+
return act;
|
|
571
|
+
}
|
|
572
|
+
async function resolvePerformer(_cwd, input) {
|
|
573
|
+
return {
|
|
574
|
+
id: input.id,
|
|
575
|
+
name: input.name,
|
|
576
|
+
model: input.model !== undefined ? input.model : null,
|
|
577
|
+
modelVariant: input.modelVariant || null,
|
|
578
|
+
agentId: input.agentId || (input.planMode ? 'plan' : 'build'),
|
|
579
|
+
talRef: input.talRef !== undefined ? input.talRef : null,
|
|
580
|
+
danceRefs: input.danceRefs !== undefined ? input.danceRefs : [],
|
|
581
|
+
mcpServerNames: input.mcpServerNames !== undefined ? Array.from(new Set(input.mcpServerNames.filter(Boolean))) : [],
|
|
582
|
+
danceDeliveryMode: input.danceDeliveryMode || 'auto',
|
|
583
|
+
planMode: !!input.planMode,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function normalizeStageActInput(act) {
|
|
587
|
+
return {
|
|
588
|
+
...act,
|
|
589
|
+
sessionMode: normalizeActSessionMode(act.sessionMode),
|
|
590
|
+
nodes: act.nodes.map((node) => {
|
|
591
|
+
if (node.type === 'parallel') {
|
|
592
|
+
return node;
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
...node,
|
|
596
|
+
sessionModeOverride: !!node.sessionModeOverride,
|
|
597
|
+
};
|
|
598
|
+
}),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function resolveActRuntimeInput(input) {
|
|
602
|
+
let stageAct = input.stageAct || null;
|
|
603
|
+
let performerInputs = input.performers || [];
|
|
604
|
+
if (!stageAct && input.actUrn) {
|
|
605
|
+
const normalized = stageActFromRegistryAct(await loadActDefinition(input.cwd, input.actUrn));
|
|
606
|
+
stageAct = normalized.stageAct;
|
|
607
|
+
performerInputs = normalized.performers;
|
|
608
|
+
}
|
|
609
|
+
if (!stageAct) {
|
|
610
|
+
throw new Error('Either stageAct or actUrn is required.');
|
|
611
|
+
}
|
|
612
|
+
const performers = await Promise.all(performerInputs.map((performer) => resolvePerformer(input.cwd, performer)));
|
|
613
|
+
return { act: normalizeStageActInput(stageAct), performers, drafts: input.drafts || {} };
|
|
614
|
+
}
|
|
615
|
+
function buildSessionScopeKey(context, policy, lifetime, nodeId, performerId) {
|
|
616
|
+
if (policy === 'fresh') {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
if (lifetime === 'thread') {
|
|
620
|
+
return buildPersistentHandle(context.act, lifetime, policy, nodeId, performerId);
|
|
621
|
+
}
|
|
622
|
+
if (policy === 'node') {
|
|
623
|
+
return `${context.runId}:node:${nodeId}`;
|
|
624
|
+
}
|
|
625
|
+
if (policy === 'performer') {
|
|
626
|
+
return `${context.runId}:performer:${performerId || 'unassigned'}`;
|
|
627
|
+
}
|
|
628
|
+
return `${context.runId}:act`;
|
|
629
|
+
}
|
|
630
|
+
async function createSession(cwd, title) {
|
|
631
|
+
const oc = await getOpencode();
|
|
632
|
+
const session = unwrapOpencodeResult(await oc.session.create({
|
|
633
|
+
directory: cwd,
|
|
634
|
+
title,
|
|
635
|
+
}));
|
|
636
|
+
return {
|
|
637
|
+
oc,
|
|
638
|
+
sessionId: session.id,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async function deleteSession(cwd, sessionId) {
|
|
642
|
+
const oc = await getOpencode();
|
|
643
|
+
await Promise.race([
|
|
644
|
+
oc.session.delete({
|
|
645
|
+
sessionID: sessionId,
|
|
646
|
+
directory: cwd,
|
|
647
|
+
}).then((result) => {
|
|
648
|
+
unwrapOpencodeResult(result);
|
|
649
|
+
}).catch(() => undefined),
|
|
650
|
+
new Promise((resolve) => {
|
|
651
|
+
setTimeout(resolve, 1500);
|
|
652
|
+
}),
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
async function resolveSession(context, policy, lifetime, nodeId, performerId, configKey, title, directive) {
|
|
656
|
+
if (directive?.mode === 'reuse') {
|
|
657
|
+
const handle = directive.handle?.trim();
|
|
658
|
+
if (!handle) {
|
|
659
|
+
throw new Error(`Node '${nodeId}' requested session reuse without a handle.`);
|
|
660
|
+
}
|
|
661
|
+
const existingThreadHandle = context.threadSessionHandles.get(handle);
|
|
662
|
+
if (!existingThreadHandle) {
|
|
663
|
+
throw new Error(`Session handle '${handle}' is not available for this act thread.`);
|
|
664
|
+
}
|
|
665
|
+
if (existingThreadHandle.configKey && existingThreadHandle.configKey !== configKey) {
|
|
666
|
+
throw new Error(`Session handle '${handle}' no longer matches the current node runtime configuration.`);
|
|
667
|
+
}
|
|
668
|
+
const oc = await getOpencode();
|
|
669
|
+
return {
|
|
670
|
+
oc,
|
|
671
|
+
sessionId: existingThreadHandle.sessionId,
|
|
672
|
+
configKey,
|
|
673
|
+
ephemeral: false,
|
|
674
|
+
source: 'thread',
|
|
675
|
+
scopeKey: handle,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (directive?.mode === 'fresh') {
|
|
679
|
+
const fresh = await createSession(context.cwd, title);
|
|
680
|
+
const persistentHandle = buildPersistentHandle(context.act, lifetime, policy, nodeId, performerId);
|
|
681
|
+
return {
|
|
682
|
+
...fresh,
|
|
683
|
+
configKey,
|
|
684
|
+
ephemeral: !persistentHandle,
|
|
685
|
+
source: persistentHandle ? 'thread' : 'fresh',
|
|
686
|
+
...(persistentHandle ? { scopeKey: persistentHandle } : {}),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const scopeKey = buildSessionScopeKey(context, policy, lifetime, nodeId, performerId);
|
|
690
|
+
if (!scopeKey) {
|
|
691
|
+
const fresh = await createSession(context.cwd, title);
|
|
692
|
+
return {
|
|
693
|
+
...fresh,
|
|
694
|
+
configKey,
|
|
695
|
+
ephemeral: true,
|
|
696
|
+
source: 'fresh',
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (lifetime === 'thread') {
|
|
700
|
+
const existingThreadHandle = context.threadSessionHandles.get(scopeKey);
|
|
701
|
+
if (existingThreadHandle && (!existingThreadHandle.configKey || existingThreadHandle.configKey === configKey)) {
|
|
702
|
+
const oc = await getOpencode();
|
|
703
|
+
return {
|
|
704
|
+
oc,
|
|
705
|
+
sessionId: existingThreadHandle.sessionId,
|
|
706
|
+
configKey,
|
|
707
|
+
scopeKey,
|
|
708
|
+
ephemeral: false,
|
|
709
|
+
source: 'thread',
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const existing = context.sessionPool.get(scopeKey);
|
|
714
|
+
if (existing && (!existing.configKey || existing.configKey === configKey)) {
|
|
715
|
+
const oc = await getOpencode();
|
|
716
|
+
return {
|
|
717
|
+
oc,
|
|
718
|
+
sessionId: existing.sessionId,
|
|
719
|
+
configKey,
|
|
720
|
+
scopeKey,
|
|
721
|
+
ephemeral: false,
|
|
722
|
+
source: 'run',
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const created = await createSession(context.cwd, title);
|
|
726
|
+
context.sessionPool.set(scopeKey, {
|
|
727
|
+
scopeKey,
|
|
728
|
+
sessionId: created.sessionId,
|
|
729
|
+
configKey,
|
|
730
|
+
policy,
|
|
731
|
+
lifetime,
|
|
732
|
+
nodeId,
|
|
733
|
+
performerId,
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
...created,
|
|
737
|
+
configKey,
|
|
738
|
+
scopeKey,
|
|
739
|
+
ephemeral: false,
|
|
740
|
+
source: 'run',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function invokePerformer(context, node, performer, input, title, directive) {
|
|
744
|
+
assertActNotAborted(context);
|
|
745
|
+
if (!performer.model) {
|
|
746
|
+
throw new Error(`Performer '${performer.name}' is missing a model.`);
|
|
747
|
+
}
|
|
748
|
+
const selectedModelVariant = node.modelVariant || performer.modelVariant || null;
|
|
749
|
+
const sessionSettings = resolveNodeSessionSettings(context.act, node);
|
|
750
|
+
const configKey = buildNodeRuntimeConfigKey(context.act, node, performer, selectedModelVariant);
|
|
751
|
+
const envelope = await buildPromptEnvelope({
|
|
752
|
+
cwd: context.cwd,
|
|
753
|
+
talRef: performer.talRef,
|
|
754
|
+
danceRefs: performer.danceRefs,
|
|
755
|
+
drafts: context.drafts,
|
|
756
|
+
model: performer.model,
|
|
757
|
+
modelVariant: selectedModelVariant,
|
|
758
|
+
danceDeliveryMode: performer.danceDeliveryMode,
|
|
759
|
+
});
|
|
760
|
+
const toolResolution = await resolveRuntimeTools(context.cwd, performer.model, performer.mcpServerNames);
|
|
761
|
+
const unavailableSummary = describeUnavailableRuntimeTools(toolResolution);
|
|
762
|
+
if (toolResolution.selectedMcpServers.length > 0 && toolResolution.resolvedTools.length === 0 && unavailableSummary) {
|
|
763
|
+
throw new Error(`Selected MCP servers are unavailable: ${unavailableSummary}.`);
|
|
764
|
+
}
|
|
765
|
+
const tools = buildEnabledToolMap([
|
|
766
|
+
...toolResolution.resolvedTools,
|
|
767
|
+
...(envelope.toolName ? [envelope.toolName] : []),
|
|
768
|
+
]);
|
|
769
|
+
const runtimeSystem = buildActRuntimeSystem(context, node);
|
|
770
|
+
const orchestratorRoutes = node.type === 'orchestrator'
|
|
771
|
+
? getOrchestratorRoutes(context.act, node.id)
|
|
772
|
+
: [];
|
|
773
|
+
const session = await resolveSession(context, sessionSettings.policy, sessionSettings.lifetime, node.id, node.performerId, configKey, title, directive);
|
|
774
|
+
emitActPerformerBinding(context, session.sessionId, node, performer);
|
|
775
|
+
try {
|
|
776
|
+
assertActNotAborted(context);
|
|
777
|
+
const result = unwrapPromptResult(await session.oc.session.prompt({
|
|
778
|
+
sessionID: session.sessionId,
|
|
779
|
+
directory: context.cwd,
|
|
780
|
+
model: { providerID: performer.model.provider, modelID: performer.model.modelId },
|
|
781
|
+
agent: performer.agentId || (performer.planMode ? 'plan' : 'build'),
|
|
782
|
+
system: runtimeSystem ? `${envelope.system}\n\n${runtimeSystem}` : envelope.system,
|
|
783
|
+
...(selectedModelVariant ? { variant: selectedModelVariant } : {}),
|
|
784
|
+
...(tools ? { tools } : {}),
|
|
785
|
+
...(node.type === 'orchestrator' ? { format: buildOrchestratorFormat(orchestratorRoutes) } : {}),
|
|
786
|
+
parts: [{ type: 'text', text: input }],
|
|
787
|
+
}));
|
|
788
|
+
return {
|
|
789
|
+
output: extractTextFromResponse(result),
|
|
790
|
+
session,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
if (isAbortRequested(context.actSessionId)) {
|
|
795
|
+
await releaseEphemeralSession(context.cwd, session, false);
|
|
796
|
+
throw new ActRuntimeInterruptedError();
|
|
797
|
+
}
|
|
798
|
+
await releaseEphemeralSession(context.cwd, session, false);
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function rememberThreadHandle(context, node, session, output) {
|
|
803
|
+
if (!context.actSessionId) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
const sessionSettings = resolveNodeSessionSettings(context.act, node);
|
|
807
|
+
const handle = session.scopeKey || buildPersistentHandle(context.act, sessionSettings.lifetime, sessionSettings.policy, node.id, node.performerId);
|
|
808
|
+
if (!handle) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
const existing = context.threadSessionHandles.get(handle);
|
|
812
|
+
if (existing && existing.sessionId !== session.sessionId && session.source !== 'thread') {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
if (existing && existing.sessionId !== session.sessionId && session.source === 'thread') {
|
|
816
|
+
await deleteSession(context.cwd, existing.sessionId);
|
|
817
|
+
}
|
|
818
|
+
context.threadSessionHandles.set(handle, {
|
|
819
|
+
handle,
|
|
820
|
+
sessionId: session.sessionId,
|
|
821
|
+
configKey: session.configKey,
|
|
822
|
+
nodeId: node.id,
|
|
823
|
+
nodeType: node.type,
|
|
824
|
+
performerId: node.performerId,
|
|
825
|
+
status: 'warm',
|
|
826
|
+
turnCount: (existing?.turnCount || 0) + 1,
|
|
827
|
+
lastUsedAt: Date.now(),
|
|
828
|
+
summary: summarizeText(output),
|
|
829
|
+
});
|
|
830
|
+
if (session.scopeKey) {
|
|
831
|
+
const pooled = context.sessionPool.get(session.scopeKey);
|
|
832
|
+
if (pooled) {
|
|
833
|
+
pooled.persistentHandle = handle;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
async function releaseEphemeralSession(cwd, session, keepAlive) {
|
|
839
|
+
if (session.ephemeral && !keepAlive) {
|
|
840
|
+
await deleteSession(cwd, session.sessionId);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function buildNodeLookup(act) {
|
|
844
|
+
return new Map(act.nodes.map((node) => [node.id, node]));
|
|
845
|
+
}
|
|
846
|
+
function transitionAfterNode(context, nodeId, outcome, output) {
|
|
847
|
+
const next = selectNextTarget(context.act, nodeId, outcome);
|
|
848
|
+
if (!next || next === '$exit') {
|
|
849
|
+
return {
|
|
850
|
+
status: outcome === 'success' ? 'completed' : 'failed',
|
|
851
|
+
context: {
|
|
852
|
+
...context,
|
|
853
|
+
currentNodeId: null,
|
|
854
|
+
pendingInput: output,
|
|
855
|
+
finalOutput: output,
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
status: 'continue',
|
|
861
|
+
context: {
|
|
862
|
+
...context,
|
|
863
|
+
currentNodeId: next,
|
|
864
|
+
pendingInput: output,
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function cleanupSessionPool(cwd, sessionPool, threadSessionHandles) {
|
|
869
|
+
const persistentSessionIds = new Set(Array.from(threadSessionHandles.values()).map((session) => session.sessionId));
|
|
870
|
+
await Promise.all(Array.from(sessionPool.values())
|
|
871
|
+
.filter((session) => !persistentSessionIds.has(session.sessionId))
|
|
872
|
+
.map((session) => deleteSession(cwd, session.sessionId)));
|
|
873
|
+
}
|
|
874
|
+
async function runBranchMachine(context, startNodeId, input) {
|
|
875
|
+
const branchContext = {
|
|
876
|
+
...cloneContext(context),
|
|
877
|
+
runId: `${context.runId}:${startNodeId}`,
|
|
878
|
+
actSessionId: null,
|
|
879
|
+
currentNodeId: startNodeId,
|
|
880
|
+
pendingInput: input,
|
|
881
|
+
iterations: 0,
|
|
882
|
+
history: [],
|
|
883
|
+
nodeOutputs: {},
|
|
884
|
+
sessionPool: new Map(),
|
|
885
|
+
pendingSessionDirective: null,
|
|
886
|
+
};
|
|
887
|
+
const result = await runActMachine(branchContext);
|
|
888
|
+
await cleanupSessionPool(context.cwd, result.context.sessionPool, result.context.threadSessionHandles);
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
async function advanceRuntimeStep(context) {
|
|
892
|
+
const nextContext = cloneContext(context);
|
|
893
|
+
if (isAbortRequested(nextContext.actSessionId)) {
|
|
894
|
+
return {
|
|
895
|
+
status: 'interrupted',
|
|
896
|
+
context: {
|
|
897
|
+
...nextContext,
|
|
898
|
+
currentNodeId: null,
|
|
899
|
+
error: 'Act run interrupted.',
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (!nextContext.currentNodeId) {
|
|
904
|
+
return {
|
|
905
|
+
status: 'failed',
|
|
906
|
+
context: {
|
|
907
|
+
...nextContext,
|
|
908
|
+
error: 'No current Act node selected.',
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (nextContext.iterations >= nextContext.maxIterations) {
|
|
913
|
+
return {
|
|
914
|
+
status: 'failed',
|
|
915
|
+
context: {
|
|
916
|
+
...nextContext,
|
|
917
|
+
currentNodeId: null,
|
|
918
|
+
error: `Act exceeded maxIterations (${nextContext.maxIterations}).`,
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const node = buildNodeLookup(nextContext.act).get(nextContext.currentNodeId);
|
|
923
|
+
if (!node) {
|
|
924
|
+
return {
|
|
925
|
+
status: 'failed',
|
|
926
|
+
context: {
|
|
927
|
+
...nextContext,
|
|
928
|
+
currentNodeId: null,
|
|
929
|
+
error: `Act node not found: ${nextContext.currentNodeId}`,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
nextContext.iterations += 1;
|
|
934
|
+
nextContext.sharedState.currentNodeId = node.id;
|
|
935
|
+
nextContext.sharedState.iterations = nextContext.iterations;
|
|
936
|
+
const timestamp = Date.now();
|
|
937
|
+
if (node.type === 'parallel') {
|
|
938
|
+
const branches = getParallelBranches(nextContext.act, node.id);
|
|
939
|
+
const branchRuns = await Promise.all(branches.map(async (branch) => {
|
|
940
|
+
try {
|
|
941
|
+
const result = await runBranchMachine(nextContext, branch, nextContext.pendingInput);
|
|
942
|
+
return { result };
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
return { error: describeActRuntimeError(error) };
|
|
946
|
+
}
|
|
947
|
+
}));
|
|
948
|
+
for (const branch of branchRuns) {
|
|
949
|
+
if (branch.result) {
|
|
950
|
+
nextContext.iterations += branch.result.context.iterations;
|
|
951
|
+
nextContext.history.push(...branch.result.context.history);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const successful = branchRuns.filter((branch) => branch.result?.status === 'completed');
|
|
955
|
+
const failed = branchRuns.filter((branch) => branch.error || branch.result?.status === 'failed');
|
|
956
|
+
const success = node.join === 'any' ? successful.length > 0 : failed.length === 0;
|
|
957
|
+
const output = success
|
|
958
|
+
? (node.join === 'any'
|
|
959
|
+
? successful[0]?.result?.context.finalOutput || ''
|
|
960
|
+
: successful.map((branch) => branch.result?.context.finalOutput || '').filter(Boolean).join('\n\n'))
|
|
961
|
+
: failed.map((branch) => branch.error || branch.result?.context.error || branch.result?.context.finalOutput || 'Parallel branch failed').join('\n\n');
|
|
962
|
+
nextContext.history.push({
|
|
963
|
+
nodeId: node.id,
|
|
964
|
+
nodeType: 'parallel',
|
|
965
|
+
action: success ? `parallel.completed:${node.join}` : `parallel.failed:${node.join}`,
|
|
966
|
+
timestamp,
|
|
967
|
+
});
|
|
968
|
+
nextContext.nodeOutputs[node.id] = output;
|
|
969
|
+
nextContext.sharedState.nodeOutputs = nextContext.nodeOutputs;
|
|
970
|
+
return transitionAfterNode(nextContext, node.id, success ? 'success' : 'fail', output);
|
|
971
|
+
}
|
|
972
|
+
const performer = node.performerId ? nextContext.performersById[node.performerId] : null;
|
|
973
|
+
if (!performer) {
|
|
974
|
+
const message = `Act node '${node.id}' does not have a resolved performer.`;
|
|
975
|
+
nextContext.history.push({
|
|
976
|
+
nodeId: node.id,
|
|
977
|
+
nodeType: node.type,
|
|
978
|
+
action: `${node.type}.failed: ${message}`,
|
|
979
|
+
timestamp,
|
|
980
|
+
});
|
|
981
|
+
return transitionAfterNode(nextContext, node.id, 'fail', message);
|
|
982
|
+
}
|
|
983
|
+
const pendingDirective = nextContext.pendingSessionDirective?.nodeId === node.id
|
|
984
|
+
? nextContext.pendingSessionDirective
|
|
985
|
+
: null;
|
|
986
|
+
nextContext.pendingSessionDirective = null;
|
|
987
|
+
if (node.type === 'worker') {
|
|
988
|
+
try {
|
|
989
|
+
const invocation = await invokePerformer(nextContext, node, performer, nextContext.pendingInput, `Worker: ${node.id}`, pendingDirective);
|
|
990
|
+
const output = invocation.output;
|
|
991
|
+
const keepAlive = await rememberThreadHandle(nextContext, node, invocation.session, output);
|
|
992
|
+
nextContext.history.push({ nodeId: node.id, nodeType: 'worker', action: 'worker.completed', timestamp });
|
|
993
|
+
nextContext.nodeOutputs[node.id] = output;
|
|
994
|
+
nextContext.sharedState.nodeOutputs = nextContext.nodeOutputs;
|
|
995
|
+
nextContext.sharedState.sessionHandles = listAvailableSessionHandles(nextContext, nextContext.act.nodes.map((item) => item.id));
|
|
996
|
+
await releaseEphemeralSession(nextContext.cwd, invocation.session, keepAlive);
|
|
997
|
+
return transitionAfterNode(nextContext, node.id, 'success', output);
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
if (isActRuntimeInterrupted(error)) {
|
|
1001
|
+
return {
|
|
1002
|
+
status: 'interrupted',
|
|
1003
|
+
context: {
|
|
1004
|
+
...nextContext,
|
|
1005
|
+
currentNodeId: null,
|
|
1006
|
+
error: 'Act run interrupted.',
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
const message = describeActRuntimeError(error, { model: performer.model });
|
|
1011
|
+
nextContext.history.push({ nodeId: node.id, nodeType: 'worker', action: `worker.failed: ${message}`, timestamp });
|
|
1012
|
+
return transitionAfterNode(nextContext, node.id, 'fail', `Worker '${node.id}' failed: ${message}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// Enforce maxDelegations for orchestrator nodes
|
|
1016
|
+
if (typeof node.maxDelegations === 'number' && node.maxDelegations > 0) {
|
|
1017
|
+
const priorDelegations = nextContext.history.filter((entry) => entry.nodeId === node.id && entry.action.startsWith('orchestrator.routed:')).length;
|
|
1018
|
+
if (priorDelegations >= node.maxDelegations) {
|
|
1019
|
+
const message = `Orchestrator '${node.id}' exceeded maxDelegations (${node.maxDelegations}).`;
|
|
1020
|
+
nextContext.history.push({
|
|
1021
|
+
nodeId: node.id,
|
|
1022
|
+
nodeType: 'orchestrator',
|
|
1023
|
+
action: `orchestrator.failed: ${message}`,
|
|
1024
|
+
timestamp,
|
|
1025
|
+
});
|
|
1026
|
+
return transitionAfterNode(nextContext, node.id, 'fail', message);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const orchestratorRoutes = getOrchestratorRoutes(nextContext.act, node.id);
|
|
1030
|
+
try {
|
|
1031
|
+
const invocation = await invokePerformer(nextContext, node, performer, buildOrchestratorPrompt(nextContext.pendingInput, orchestratorRoutes, listAvailableSessionHandles(nextContext, orchestratorRoutes)), `Orchestrator: ${node.id}`, pendingDirective);
|
|
1032
|
+
const response = invocation.output;
|
|
1033
|
+
const decision = parseOrchestratorDecision(response, orchestratorRoutes);
|
|
1034
|
+
if (decision.next !== '$exit' && decision.session?.mode === 'reuse') {
|
|
1035
|
+
const handle = decision.session.handle?.trim();
|
|
1036
|
+
if (!handle) {
|
|
1037
|
+
throw new Error(`Orchestrator selected session reuse for '${decision.next}' without a handle.`);
|
|
1038
|
+
}
|
|
1039
|
+
const handleRecord = nextContext.threadSessionHandles.get(handle);
|
|
1040
|
+
if (!handleRecord) {
|
|
1041
|
+
throw new Error(`Orchestrator selected unavailable session handle '${handle}'.`);
|
|
1042
|
+
}
|
|
1043
|
+
if (handleRecord.nodeId !== decision.next) {
|
|
1044
|
+
throw new Error(`Session handle '${handle}' belongs to '${handleRecord.nodeId}', not '${decision.next}'.`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const keepAlive = await rememberThreadHandle(nextContext, node, invocation.session, response);
|
|
1048
|
+
await releaseEphemeralSession(nextContext.cwd, invocation.session, keepAlive);
|
|
1049
|
+
nextContext.history.push({
|
|
1050
|
+
nodeId: node.id,
|
|
1051
|
+
nodeType: 'orchestrator',
|
|
1052
|
+
action: `orchestrator.routed:${decision.next}`,
|
|
1053
|
+
timestamp,
|
|
1054
|
+
});
|
|
1055
|
+
nextContext.nodeOutputs[node.id] = decision.input || nextContext.pendingInput;
|
|
1056
|
+
nextContext.sharedState.nodeOutputs = nextContext.nodeOutputs;
|
|
1057
|
+
nextContext.sharedState.sessionHandles = listAvailableSessionHandles(nextContext, nextContext.act.nodes.map((item) => item.id));
|
|
1058
|
+
if (decision.next === '$exit') {
|
|
1059
|
+
return {
|
|
1060
|
+
status: 'completed',
|
|
1061
|
+
context: {
|
|
1062
|
+
...nextContext,
|
|
1063
|
+
currentNodeId: null,
|
|
1064
|
+
pendingSessionDirective: null,
|
|
1065
|
+
pendingInput: decision.input || nextContext.pendingInput,
|
|
1066
|
+
finalOutput: decision.input || nextContext.pendingInput,
|
|
1067
|
+
},
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
status: 'continue',
|
|
1072
|
+
context: {
|
|
1073
|
+
...nextContext,
|
|
1074
|
+
currentNodeId: decision.next,
|
|
1075
|
+
pendingInput: decision.input || nextContext.pendingInput,
|
|
1076
|
+
pendingSessionDirective: decision.next === '$exit' || !decision.session
|
|
1077
|
+
? null
|
|
1078
|
+
: {
|
|
1079
|
+
nodeId: decision.next,
|
|
1080
|
+
mode: decision.session.mode,
|
|
1081
|
+
...(decision.session.handle ? { handle: decision.session.handle } : {}),
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
if (isActRuntimeInterrupted(error)) {
|
|
1088
|
+
return {
|
|
1089
|
+
status: 'interrupted',
|
|
1090
|
+
context: {
|
|
1091
|
+
...nextContext,
|
|
1092
|
+
currentNodeId: null,
|
|
1093
|
+
error: 'Act run interrupted.',
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const message = describeActRuntimeError(error, { model: performer.model });
|
|
1098
|
+
nextContext.history.push({
|
|
1099
|
+
nodeId: node.id,
|
|
1100
|
+
nodeType: 'orchestrator',
|
|
1101
|
+
action: `orchestrator.failed: ${message}`,
|
|
1102
|
+
timestamp,
|
|
1103
|
+
});
|
|
1104
|
+
return transitionAfterNode(nextContext, node.id, 'fail', `Orchestrator '${node.id}' failed: ${message}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
const actRuntimeMachine = setup({
|
|
1108
|
+
types: {
|
|
1109
|
+
context: {},
|
|
1110
|
+
input: {},
|
|
1111
|
+
output: {},
|
|
1112
|
+
},
|
|
1113
|
+
actors: {
|
|
1114
|
+
advance: fromPromise(async ({ input }) => advanceRuntimeStep(input)),
|
|
1115
|
+
},
|
|
1116
|
+
}).createMachine({
|
|
1117
|
+
id: 'act-runtime',
|
|
1118
|
+
initial: 'executing',
|
|
1119
|
+
context: ({ input }) => input,
|
|
1120
|
+
states: {
|
|
1121
|
+
executing: {
|
|
1122
|
+
invoke: {
|
|
1123
|
+
src: 'advance',
|
|
1124
|
+
input: ({ context }) => context,
|
|
1125
|
+
onDone: [
|
|
1126
|
+
{
|
|
1127
|
+
guard: ({ event }) => event.output.status === 'continue',
|
|
1128
|
+
actions: assign(({ event }) => event.output.context),
|
|
1129
|
+
target: 'executing',
|
|
1130
|
+
reenter: true,
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
guard: ({ event }) => event.output.status === 'completed',
|
|
1134
|
+
actions: assign(({ event }) => event.output.context),
|
|
1135
|
+
target: 'completed',
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
guard: ({ event }) => event.output.status === 'interrupted',
|
|
1139
|
+
actions: assign(({ event }) => event.output.context),
|
|
1140
|
+
target: 'interrupted',
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
actions: assign(({ event }) => event.output.context),
|
|
1144
|
+
target: 'failed',
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
completed: {
|
|
1150
|
+
type: 'final',
|
|
1151
|
+
output: ({ context }) => ({ status: 'completed', context }),
|
|
1152
|
+
},
|
|
1153
|
+
interrupted: {
|
|
1154
|
+
type: 'final',
|
|
1155
|
+
output: ({ context }) => ({ status: 'interrupted', context }),
|
|
1156
|
+
},
|
|
1157
|
+
failed: {
|
|
1158
|
+
type: 'final',
|
|
1159
|
+
output: ({ context }) => ({ status: 'failed', context }),
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
async function runActMachine(input, onProgress) {
|
|
1164
|
+
const actor = createActor(actRuntimeMachine, { input });
|
|
1165
|
+
let previousSignature = '';
|
|
1166
|
+
actor.subscribe((snapshot) => {
|
|
1167
|
+
if (!onProgress) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const context = snapshot.context;
|
|
1171
|
+
const signature = [
|
|
1172
|
+
context.currentNodeId || '',
|
|
1173
|
+
context.iterations,
|
|
1174
|
+
context.history.length,
|
|
1175
|
+
context.finalOutput || '',
|
|
1176
|
+
context.error || '',
|
|
1177
|
+
].join('::');
|
|
1178
|
+
if (signature === previousSignature) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
previousSignature = signature;
|
|
1182
|
+
onProgress(context);
|
|
1183
|
+
});
|
|
1184
|
+
actor.start();
|
|
1185
|
+
await toPromise(actor);
|
|
1186
|
+
const snapshot = actor.getSnapshot();
|
|
1187
|
+
return {
|
|
1188
|
+
status: snapshot.value === 'completed'
|
|
1189
|
+
? 'completed'
|
|
1190
|
+
: snapshot.value === 'interrupted'
|
|
1191
|
+
? 'interrupted'
|
|
1192
|
+
: 'failed',
|
|
1193
|
+
context: snapshot.context,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
export async function runActRuntime(input) {
|
|
1197
|
+
if (input.actSessionId) {
|
|
1198
|
+
actRuntimeAbortRequests.delete(input.actSessionId);
|
|
1199
|
+
}
|
|
1200
|
+
const resolved = await resolveActRuntimeInput(input);
|
|
1201
|
+
const performersById = Object.fromEntries(resolved.performers.map((performer) => [performer.id, performer]));
|
|
1202
|
+
const threadSessionHandles = getThreadRuntimeHandles(input.actSessionId, resolved.act.id);
|
|
1203
|
+
const coldStartResumeSummary = threadSessionHandles.size === 0 ? input.resumeSummary || null : null;
|
|
1204
|
+
const initialContext = {
|
|
1205
|
+
runId: makeId('run'),
|
|
1206
|
+
actSessionId: input.actSessionId,
|
|
1207
|
+
cwd: input.cwd,
|
|
1208
|
+
act: resolved.act,
|
|
1209
|
+
performersById,
|
|
1210
|
+
drafts: resolved.drafts,
|
|
1211
|
+
currentNodeId: resolved.act.entryNodeId,
|
|
1212
|
+
pendingInput: input.input,
|
|
1213
|
+
maxIterations: input.maxIterations || resolved.act.maxIterations || 10,
|
|
1214
|
+
iterations: 0,
|
|
1215
|
+
history: [],
|
|
1216
|
+
sharedState: {
|
|
1217
|
+
sessionHandles: Array.from(threadSessionHandles.values()).map((session) => ({
|
|
1218
|
+
handle: session.handle,
|
|
1219
|
+
nodeId: session.nodeId,
|
|
1220
|
+
nodeType: session.nodeType,
|
|
1221
|
+
performerId: session.performerId,
|
|
1222
|
+
status: session.status,
|
|
1223
|
+
turnCount: session.turnCount,
|
|
1224
|
+
lastUsedAt: session.lastUsedAt,
|
|
1225
|
+
summary: session.summary,
|
|
1226
|
+
})),
|
|
1227
|
+
...(coldStartResumeSummary ? {
|
|
1228
|
+
previousThreadSummary: {
|
|
1229
|
+
runId: coldStartResumeSummary.runId || null,
|
|
1230
|
+
currentNodeId: coldStartResumeSummary.currentNodeId || null,
|
|
1231
|
+
finalOutput: coldStartResumeSummary.finalOutput || null,
|
|
1232
|
+
error: coldStartResumeSummary.error || null,
|
|
1233
|
+
iterations: coldStartResumeSummary.iterations || 0,
|
|
1234
|
+
},
|
|
1235
|
+
} : {}),
|
|
1236
|
+
},
|
|
1237
|
+
nodeOutputs: {},
|
|
1238
|
+
resumeSummary: coldStartResumeSummary,
|
|
1239
|
+
sessionPool: new Map(),
|
|
1240
|
+
threadSessionHandles,
|
|
1241
|
+
pendingSessionDirective: null,
|
|
1242
|
+
};
|
|
1243
|
+
emitActRuntimeProgress(initialContext, 'running');
|
|
1244
|
+
const result = await runActMachine(initialContext, (context) => {
|
|
1245
|
+
emitActRuntimeProgress(context, 'running');
|
|
1246
|
+
});
|
|
1247
|
+
persistThreadRuntimeHandles(input.actSessionId, resolved.act.id, result.context.threadSessionHandles);
|
|
1248
|
+
emitActRuntimeProgress(result.context, result.status);
|
|
1249
|
+
const response = {
|
|
1250
|
+
status: result.status,
|
|
1251
|
+
currentNodeId: result.context.currentNodeId,
|
|
1252
|
+
runId: result.context.runId,
|
|
1253
|
+
finalOutput: result.context.finalOutput,
|
|
1254
|
+
error: result.context.error,
|
|
1255
|
+
history: result.context.history,
|
|
1256
|
+
sharedState: result.context.sharedState,
|
|
1257
|
+
sessions: Array.from(result.context.sessionPool.values()).map((session) => ({
|
|
1258
|
+
scopeKey: session.scopeKey,
|
|
1259
|
+
sessionId: session.sessionId,
|
|
1260
|
+
policy: session.policy,
|
|
1261
|
+
lifetime: session.lifetime,
|
|
1262
|
+
nodeId: session.nodeId,
|
|
1263
|
+
performerId: session.performerId,
|
|
1264
|
+
})),
|
|
1265
|
+
sessionHandles: Array.from(result.context.threadSessionHandles.values()).map((session) => ({
|
|
1266
|
+
handle: session.handle,
|
|
1267
|
+
nodeId: session.nodeId,
|
|
1268
|
+
nodeType: session.nodeType,
|
|
1269
|
+
performerId: session.performerId,
|
|
1270
|
+
status: session.status,
|
|
1271
|
+
turnCount: session.turnCount,
|
|
1272
|
+
lastUsedAt: session.lastUsedAt,
|
|
1273
|
+
summary: session.summary,
|
|
1274
|
+
})),
|
|
1275
|
+
iterations: result.context.iterations,
|
|
1276
|
+
};
|
|
1277
|
+
await cleanupSessionPool(initialContext.cwd, result.context.sessionPool, result.context.threadSessionHandles);
|
|
1278
|
+
if (input.actSessionId) {
|
|
1279
|
+
actRuntimeAbortRequests.delete(input.actSessionId);
|
|
1280
|
+
}
|
|
1281
|
+
return response;
|
|
1282
|
+
}
|