@myagentroam/node 0.11.39 → 0.11.41
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/dist/connector.js +176 -21
- package/dist/database.js +35 -11
- package/dist/main.js +12 -1
- package/dist/marctl-guidance.js +18 -27
- package/dist/migrations/v022.js +58 -0
- package/dist/runner/abstract-runner.js +14 -0
- package/dist/runner/claude/tool-normalizer.js +6 -1
- package/dist/runner/codex/conversation-parser.js +23 -4
- package/dist/runner/codex/managed-run-controller.js +4 -2
- package/dist/runner/codex/tool-normalizer.js +9 -0
- package/dist/runner/execution-presentation.js +56 -0
- package/dist/runner/mar-agent/agent-network-extension.js +168 -0
- package/dist/runner/mar-agent/agent-network-wait-output.js +72 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +232 -118
- package/dist/runner/mar-agent/managed-run-controller.js +5 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -3
- package/dist/runner/mar-agent/runtime-model-factory.js +1 -0
- package/dist/runner/mar-agent/session-runtime.js +34 -1
- package/dist/runner/mar-agent-runner.js +36 -0
- package/dist/runner/opencode/tool-normalizer.js +6 -1
- package/dist/runtime-state.js +57 -0
- package/dist/service/agent-network-bindings.js +483 -0
- package/dist/service/agent-network-goal.js +19 -0
- package/dist/service/agent-network-invoke.js +179 -0
- package/dist/service/agent-network-observations.js +377 -0
- package/dist/service/agent-network-service.js +315 -0
- package/dist/service/agent-network-wait.js +130 -0
- package/dist/service/composer-message-input.js +9 -2
- package/dist/service/continuous-run-coordinator.js +24 -1
- package/dist/service/goal-continuation-coordinator.js +12 -2
- package/dist/service/marctl-caller.js +80 -0
- package/dist/service/marctl-execution-credential-control.js +65 -0
- package/dist/service/model-usage-service.js +9 -0
- package/dist/service/node-control-message-service.js +10 -0
- package/dist/service/node-request-service.js +52 -7
- package/dist/service/node-upgrade-coordinator.js +79 -8
- package/dist/service/node-upgrade-credential-control.js +57 -0
- package/dist/service/node-upgrade-reporter.js +108 -0
- package/dist/service/run-event-service.js +8 -0
- package/dist/service/run-workbench-service.js +11 -0
- package/dist/service/supervised-node-child.js +153 -0
- package/dist/service/supervisor-upgrade-handoff.js +104 -0
- package/dist/session/native-session-projection-service.js +69 -6
- package/dist/session/operation-admission.js +7 -0
- package/dist/session/runtime/conversation-projection.js +25 -3
- package/dist/session/session-catalog-service.js +156 -32
- package/dist/session/session-lifecycle-service.js +68 -42
- package/dist/session/session-message-service.js +16 -6
- package/dist/supervisor.js +209 -20
- package/dist/terminal.js +41 -21
- package/package.json +5 -5
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { actorForApprovedAgentCall, actorForAuthenticatedTerminal } from './agent-network-bindings.js';
|
|
2
|
+
import { isPlainRecord } from '../util/records.js';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { modelUsageGrantSchema } from '@myagentroam/protocol';
|
|
5
|
+
/** Bind each native invocation to the actual live Node Execution and its captured resources. */
|
|
6
|
+
export function agentNetworkInvoke(input) {
|
|
7
|
+
return async (executionId, action, payload, context) => {
|
|
8
|
+
const actor = executionAgentActor({ ...input, executionId });
|
|
9
|
+
return invokeAgentNetwork(input.service, actor, action, payload, context);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Read actor resources from the current Node-owned Execution, not caller arguments. */
|
|
13
|
+
export function executionAgentActor(input) {
|
|
14
|
+
const { sessionId, executionId } = input;
|
|
15
|
+
const execution = input.runtime.executionSnapshot(executionId);
|
|
16
|
+
const prepared = input.coordinator.pending(executionId);
|
|
17
|
+
const run = execution && input.runtime.getRun(execution.runId);
|
|
18
|
+
if (!execution ||
|
|
19
|
+
!prepared ||
|
|
20
|
+
!run ||
|
|
21
|
+
execution.sessionId !== sessionId ||
|
|
22
|
+
prepared.sessionId !== sessionId ||
|
|
23
|
+
input.runtime.activeSessionRun(sessionId)?.id !== run.id ||
|
|
24
|
+
input.runtime.currentExecutionId(run.id) !== executionId ||
|
|
25
|
+
!['STARTING', 'RUNNING'].includes(execution.status) ||
|
|
26
|
+
!Number.isSafeInteger(run.initiatedByUserId))
|
|
27
|
+
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
28
|
+
const captured = prepared.executionContext.runner === 'mar-agent' &&
|
|
29
|
+
isPlainRecord(prepared.executionContext.runnerInput)
|
|
30
|
+
? prepared.executionContext.runnerInput
|
|
31
|
+
: {};
|
|
32
|
+
return {
|
|
33
|
+
sessionId,
|
|
34
|
+
runId: run.id,
|
|
35
|
+
executionId,
|
|
36
|
+
...(run.initiatedByUserId === undefined ? {} : { requestUserId: run.initiatedByUserId }),
|
|
37
|
+
secretEnvironment: prepared.secretEnvironment,
|
|
38
|
+
...(prepared.usageGrant === undefined ? {} : { usageGrant: prepared.usageGrant }),
|
|
39
|
+
...(prepared.personalInstructions === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { personalInstructions: prepared.personalInstructions }),
|
|
42
|
+
runnerConfiguration: {
|
|
43
|
+
marAgentModel: captured.model,
|
|
44
|
+
marAgentModels: captured.models,
|
|
45
|
+
marAgentCompact: captured.compact,
|
|
46
|
+
marAgentRuntimeScopeId: captured.runtimeScopeId
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Shared command dispatch; callers must bind actor authority at their trusted boundary. */
|
|
51
|
+
export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal) {
|
|
52
|
+
const targets = typeof payload.agentId === 'string'
|
|
53
|
+
? [payload.agentId]
|
|
54
|
+
: Array.isArray(payload.agentIds)
|
|
55
|
+
? payload.agentIds.filter((id) => typeof id === 'string')
|
|
56
|
+
: [];
|
|
57
|
+
const operations = action === 'send'
|
|
58
|
+
? ['send', 'goal']
|
|
59
|
+
: action === 'stop'
|
|
60
|
+
? ['stop']
|
|
61
|
+
: action === 'wait'
|
|
62
|
+
? ['wait']
|
|
63
|
+
: [];
|
|
64
|
+
const authorized = context
|
|
65
|
+
? actorForApprovedAgentCall(actor, context, targets, operations)
|
|
66
|
+
: actor;
|
|
67
|
+
const signal = context?.signal ?? requestSignal;
|
|
68
|
+
switch (action) {
|
|
69
|
+
case 'start':
|
|
70
|
+
if ((context || actor.executionId) &&
|
|
71
|
+
(typeof payload.title !== 'string' || !payload.title.trim()))
|
|
72
|
+
throw new Error('AGENT_START_INVALID');
|
|
73
|
+
if (!context &&
|
|
74
|
+
!actor.executionId &&
|
|
75
|
+
typeof payload.title === 'string' &&
|
|
76
|
+
!payload.title.trim()) {
|
|
77
|
+
const { title: _title, ...withoutTitle } = payload;
|
|
78
|
+
return service.start(actor, withoutTitle, signal);
|
|
79
|
+
}
|
|
80
|
+
return service.start(actor, {
|
|
81
|
+
...payload,
|
|
82
|
+
...(typeof payload.title === 'string' ? { title: payload.title.trim() } : {})
|
|
83
|
+
}, signal);
|
|
84
|
+
case 'send':
|
|
85
|
+
return service.send(authorized, payload, signal);
|
|
86
|
+
case 'stop':
|
|
87
|
+
return service.stop(authorized, payload);
|
|
88
|
+
case 'wait':
|
|
89
|
+
return service.wait(authorized, {
|
|
90
|
+
...payload,
|
|
91
|
+
...(context
|
|
92
|
+
? { signal: context.signal, interrupt: context.waitInterrupt }
|
|
93
|
+
: requestSignal
|
|
94
|
+
? { signal: requestSignal }
|
|
95
|
+
: {})
|
|
96
|
+
});
|
|
97
|
+
case 'list':
|
|
98
|
+
return service.list(actor, payload);
|
|
99
|
+
case 'view':
|
|
100
|
+
return service.view(actor, payload);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Node broker injects the caller only after validating the Server-issued Execution credential. */
|
|
104
|
+
export function agentNetworkExecutionCommand(input) {
|
|
105
|
+
return async (data) => {
|
|
106
|
+
if (!isPlainRecord(data) ||
|
|
107
|
+
!isPlainRecord(data.__marctlCaller) ||
|
|
108
|
+
typeof data.__requestUserId !== 'number' ||
|
|
109
|
+
typeof data.workspaceId !== 'string' ||
|
|
110
|
+
!['start', 'send', 'wait', 'stop', 'list', 'view'].includes(String(data.action)) ||
|
|
111
|
+
!isPlainRecord(data.input))
|
|
112
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
113
|
+
if (data.__marctlCaller.type === 'terminal') {
|
|
114
|
+
if (!input.observations ||
|
|
115
|
+
!Number.isSafeInteger(data.__requestUserId) ||
|
|
116
|
+
data.__requestUserId <= 0)
|
|
117
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
118
|
+
const observationId = `terminal:${randomUUID()}`;
|
|
119
|
+
const observer = input.observations.attach(observationId);
|
|
120
|
+
const environment = isPlainRecord(data.secretEnvironment) &&
|
|
121
|
+
Object.values(data.secretEnvironment).every((value) => typeof value === 'string')
|
|
122
|
+
? data.secretEnvironment
|
|
123
|
+
: {};
|
|
124
|
+
const usageGrant = modelUsageGrantSchema.safeParse(data.__usageGrant);
|
|
125
|
+
const actor = actorForAuthenticatedTerminal({
|
|
126
|
+
sessionId: '',
|
|
127
|
+
observationId,
|
|
128
|
+
requestUserId: data.__requestUserId,
|
|
129
|
+
secretEnvironment: environment,
|
|
130
|
+
runnerConfiguration: {
|
|
131
|
+
marAgentModel: data.marAgentModel,
|
|
132
|
+
marAgentModels: data.marAgentModels,
|
|
133
|
+
marAgentCompact: data.marAgentCompact,
|
|
134
|
+
marAgentRuntimeScopeId: data.marAgentRuntimeScopeId
|
|
135
|
+
},
|
|
136
|
+
...(usageGrant.success ? { usageGrant: usageGrant.data } : {}),
|
|
137
|
+
...(typeof data.personalInstructions === 'string'
|
|
138
|
+
? { personalInstructions: data.personalInstructions }
|
|
139
|
+
: {})
|
|
140
|
+
}, data.workspaceId);
|
|
141
|
+
try {
|
|
142
|
+
return await invokeAgentNetwork(input.service, actor, data.action, data.input, undefined, data.__requestAbortSignal instanceof AbortSignal ? data.__requestAbortSignal : undefined);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
observer.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (data.__marctlCaller.type !== 'execution' ||
|
|
149
|
+
typeof data.__marctlCaller.sessionId !== 'string' ||
|
|
150
|
+
typeof data.__marctlCaller.executionId !== 'string')
|
|
151
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
152
|
+
const actor = executionAgentActor({
|
|
153
|
+
...input,
|
|
154
|
+
sessionId: data.__marctlCaller.sessionId,
|
|
155
|
+
executionId: data.__marctlCaller.executionId
|
|
156
|
+
});
|
|
157
|
+
const session = input.runtime.getAgentSession(actor.sessionId);
|
|
158
|
+
if (actor.requestUserId !== data.__requestUserId || session?.workspaceId !== data.workspaceId)
|
|
159
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
160
|
+
const profileQuery = data.action === 'list' && data.input.scope === 'profile' && data.input.runner === 'mar-agent';
|
|
161
|
+
const resources = (['start', 'send'].includes(String(data.action)) || profileQuery) &&
|
|
162
|
+
isPlainRecord(data.__marctlExecutionResources)
|
|
163
|
+
? data.__marctlExecutionResources
|
|
164
|
+
: undefined;
|
|
165
|
+
const boundActor = resources === undefined
|
|
166
|
+
? actor
|
|
167
|
+
: {
|
|
168
|
+
...actor,
|
|
169
|
+
runnerConfiguration: {
|
|
170
|
+
...actor.runnerConfiguration,
|
|
171
|
+
marAgentModel: resources.marAgentModel,
|
|
172
|
+
marAgentModels: resources.marAgentModels,
|
|
173
|
+
marAgentCompact: resources.marAgentCompact,
|
|
174
|
+
marAgentRuntimeScopeId: resources.marAgentRuntimeScopeId
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return invokeAgentNetwork(input.service, boundActor, data.action, data.input, undefined, data.__requestAbortSignal instanceof AbortSignal ? data.__requestAbortSignal : undefined);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { isPlainRecord } from '../util/records.js';
|
|
2
|
+
export function agentObservationFromRunEvent(input) {
|
|
3
|
+
if (input.terminalStatus !== undefined)
|
|
4
|
+
return { kind: 'terminal', value: input.terminalStatus };
|
|
5
|
+
if (input.eventType === 'conversation.item' &&
|
|
6
|
+
isPlainRecord(input.payload) &&
|
|
7
|
+
input.payload.kind === 'assistant_message' &&
|
|
8
|
+
input.payload.status === 'COMPLETED' &&
|
|
9
|
+
isPlainRecord(input.payload.payload) &&
|
|
10
|
+
typeof input.payload.payload.text === 'string' &&
|
|
11
|
+
input.payload.payload.text.length > 0)
|
|
12
|
+
return {
|
|
13
|
+
kind: 'message',
|
|
14
|
+
value: input.payload.payload.text,
|
|
15
|
+
...(input.payload.payload.phase === 'final_answer' ? { final: true } : {}),
|
|
16
|
+
...(typeof input.payload.turnId === 'string' ? { turnId: input.payload.turnId } : {})
|
|
17
|
+
};
|
|
18
|
+
if (input.eventType === 'conversation.item' &&
|
|
19
|
+
isPlainRecord(input.payload) &&
|
|
20
|
+
input.payload.kind === 'file_change' &&
|
|
21
|
+
input.payload.status === 'COMPLETED' &&
|
|
22
|
+
isPlainRecord(input.payload.payload) &&
|
|
23
|
+
Array.isArray(input.payload.payload.changes)) {
|
|
24
|
+
const paths = input.payload.payload.changes.flatMap((change) => isPlainRecord(change) && typeof change.path === 'string' && change.path.length > 0
|
|
25
|
+
? [change.path]
|
|
26
|
+
: []);
|
|
27
|
+
if (paths.length)
|
|
28
|
+
return { kind: 'files', paths };
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Volatile, session-owned delivery of live direct-child observations. */
|
|
33
|
+
export class AgentNetworkObservations {
|
|
34
|
+
#parents = new Map();
|
|
35
|
+
attach(parentId) {
|
|
36
|
+
if (this.#parents.has(parentId))
|
|
37
|
+
throw new Error('AGENT_NETWORK_PARENT_ALREADY_ATTACHED');
|
|
38
|
+
const parent = new ParentObservations(() => {
|
|
39
|
+
for (const [id, current] of this.#parents)
|
|
40
|
+
if (current === parent)
|
|
41
|
+
this.#parents.delete(id);
|
|
42
|
+
});
|
|
43
|
+
this.#parents.set(parentId, parent);
|
|
44
|
+
return parent;
|
|
45
|
+
}
|
|
46
|
+
get(parentId) {
|
|
47
|
+
return this.#parents.get(parentId);
|
|
48
|
+
}
|
|
49
|
+
publish(input) {
|
|
50
|
+
this.#parents.get(input.parentId)?.publish(input);
|
|
51
|
+
}
|
|
52
|
+
/** Direct children are automatic; other Sessions are watched only when explicitly targeted. */
|
|
53
|
+
publishChild(input) {
|
|
54
|
+
for (const [parentId, parent] of this.#parents) {
|
|
55
|
+
if (parentId === input.parentId || parent.observes(input.childId))
|
|
56
|
+
parent.publish(input);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
promote(previousId, nextId) {
|
|
60
|
+
if (previousId === nextId)
|
|
61
|
+
return;
|
|
62
|
+
const parent = this.#parents.get(previousId);
|
|
63
|
+
if (parent === undefined)
|
|
64
|
+
return;
|
|
65
|
+
if (this.#parents.has(nextId))
|
|
66
|
+
throw new Error('AGENT_NETWORK_PARENT_ALREADY_ATTACHED');
|
|
67
|
+
this.#parents.delete(previousId);
|
|
68
|
+
this.#parents.set(nextId, parent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class ParentObservations {
|
|
72
|
+
detach;
|
|
73
|
+
#items = new Map();
|
|
74
|
+
#results = new Map();
|
|
75
|
+
#listeners = new Set();
|
|
76
|
+
#wakeListeners = new Set();
|
|
77
|
+
#targets = new Set();
|
|
78
|
+
#revision = 0;
|
|
79
|
+
#closed = false;
|
|
80
|
+
constructor(detach) {
|
|
81
|
+
this.detach = detach;
|
|
82
|
+
}
|
|
83
|
+
observe(childId) {
|
|
84
|
+
if (!this.#closed)
|
|
85
|
+
this.#targets.add(childId);
|
|
86
|
+
}
|
|
87
|
+
observes(childId) {
|
|
88
|
+
return this.#targets.has(childId);
|
|
89
|
+
}
|
|
90
|
+
wake = {
|
|
91
|
+
pending: () => [...this.#items.values()].some((item) => item.kind === 'message' && item.wakes !== false),
|
|
92
|
+
subscribe: (listener) => {
|
|
93
|
+
if (!this.#closed)
|
|
94
|
+
this.#wakeListeners.add(listener);
|
|
95
|
+
return () => this.#wakeListeners.delete(listener);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** Queue an upward report for natural collection without waking the parent. */
|
|
99
|
+
report(input) {
|
|
100
|
+
if (this.#closed)
|
|
101
|
+
return false;
|
|
102
|
+
const value = boundedUtf8(input.value, 2000);
|
|
103
|
+
this.#items.set(JSON.stringify([input.childId, input.runId, input.executionId, 'report']), {
|
|
104
|
+
revision: ++this.#revision,
|
|
105
|
+
childId: input.childId,
|
|
106
|
+
runId: input.runId,
|
|
107
|
+
executionId: input.executionId,
|
|
108
|
+
kind: 'message',
|
|
109
|
+
value: value.value,
|
|
110
|
+
truncated: value.truncated,
|
|
111
|
+
wakes: false
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/** Waiters also need terminal notifications; sleeping model turns only wake for messages. */
|
|
116
|
+
subscribe(listener) {
|
|
117
|
+
if (!this.#closed)
|
|
118
|
+
this.#listeners.add(listener);
|
|
119
|
+
return () => this.#listeners.delete(listener);
|
|
120
|
+
}
|
|
121
|
+
publish(input) {
|
|
122
|
+
if (this.#closed)
|
|
123
|
+
return;
|
|
124
|
+
const { childId, runId, executionId, kind } = input;
|
|
125
|
+
const resultKey = JSON.stringify([childId, runId, executionId]);
|
|
126
|
+
if (kind === 'terminal' && input.value !== 'SUCCEEDED') {
|
|
127
|
+
const result = this.#results.get(resultKey);
|
|
128
|
+
if (result) {
|
|
129
|
+
result.revision = ++this.#revision;
|
|
130
|
+
delete result.summary;
|
|
131
|
+
delete result.summaryTruncated;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (kind === 'files' || (kind === 'message' && input.final)) {
|
|
135
|
+
const result = this.#results.get(resultKey) ?? { changedFiles: [], revision: 0 };
|
|
136
|
+
result.revision = ++this.#revision;
|
|
137
|
+
if (kind === 'files') {
|
|
138
|
+
for (const path of input.paths) {
|
|
139
|
+
if (result.changedFiles.includes(path))
|
|
140
|
+
continue;
|
|
141
|
+
if (result.changedFiles.length >= 200) {
|
|
142
|
+
result.changedFilesTruncated = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
result.changedFiles.push(path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const summary = boundedUtf8(input.value, 16_384);
|
|
150
|
+
result.summary = summary.value;
|
|
151
|
+
if (summary.truncated)
|
|
152
|
+
result.summaryTruncated = true;
|
|
153
|
+
else
|
|
154
|
+
delete result.summaryTruncated;
|
|
155
|
+
if (input.turnId)
|
|
156
|
+
result.turnId = input.turnId;
|
|
157
|
+
else
|
|
158
|
+
delete result.turnId;
|
|
159
|
+
}
|
|
160
|
+
this.#results.set(resultKey, result);
|
|
161
|
+
}
|
|
162
|
+
if (kind === 'files')
|
|
163
|
+
return;
|
|
164
|
+
const { value } = input;
|
|
165
|
+
const bounded = boundedUtf8(value, 2_000);
|
|
166
|
+
const key = JSON.stringify([childId, runId, executionId, kind]);
|
|
167
|
+
this.#items.set(key, {
|
|
168
|
+
revision: ++this.#revision,
|
|
169
|
+
childId,
|
|
170
|
+
runId,
|
|
171
|
+
executionId,
|
|
172
|
+
kind,
|
|
173
|
+
value: bounded.value,
|
|
174
|
+
truncated: bounded.truncated,
|
|
175
|
+
...(input.final ? { final: true } : {})
|
|
176
|
+
});
|
|
177
|
+
const terminalSnapshot = kind === 'terminal' ? this.capture(childId, executionId) : undefined;
|
|
178
|
+
for (const listener of [...this.#listeners]) {
|
|
179
|
+
try {
|
|
180
|
+
listener(terminalSnapshot);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// An observer must not interrupt authoritative Run publication or cleanup.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'message')
|
|
187
|
+
for (const listener of [...this.#wakeListeners]) {
|
|
188
|
+
try {
|
|
189
|
+
listener();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* A wake observer cannot affect Run publication. */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
collect(signal) {
|
|
197
|
+
signal.throwIfAborted();
|
|
198
|
+
const children = [...new Set([...this.#items.values()].map((item) => item.childId))].slice(0, 8);
|
|
199
|
+
return children.map((childId) => {
|
|
200
|
+
const snapshot = this.capture(childId);
|
|
201
|
+
const updates = snapshot.result?.summary && !snapshot.result.summaryTruncated
|
|
202
|
+
? snapshot.updates.filter((item) => !item.final)
|
|
203
|
+
: snapshot.updates;
|
|
204
|
+
const content = boundedObservation({
|
|
205
|
+
agentId: snapshot.childId,
|
|
206
|
+
runId: snapshot.runId,
|
|
207
|
+
executionId: snapshot.executionId,
|
|
208
|
+
...snapshot.result,
|
|
209
|
+
updates: updates.map((item) => ({
|
|
210
|
+
kind: item.kind,
|
|
211
|
+
value: item.value,
|
|
212
|
+
...(item.truncated ? { truncated: true } : {})
|
|
213
|
+
}))
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
content,
|
|
217
|
+
acknowledge: snapshot.acknowledge
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Capture the same versions used by automatic collection for wait/send consumption. */
|
|
222
|
+
capture(childId, executionId) {
|
|
223
|
+
const group = [];
|
|
224
|
+
for (const [key, item] of this.#items) {
|
|
225
|
+
if (item.childId !== childId ||
|
|
226
|
+
(executionId !== undefined && item.executionId !== executionId))
|
|
227
|
+
continue;
|
|
228
|
+
const first = group[0]?.[1];
|
|
229
|
+
if (first && (first.runId !== item.runId || first.executionId !== item.executionId))
|
|
230
|
+
continue;
|
|
231
|
+
group.push([key, item]);
|
|
232
|
+
}
|
|
233
|
+
const first = group[0]?.[1];
|
|
234
|
+
if (!first)
|
|
235
|
+
return undefined;
|
|
236
|
+
const resultKey = JSON.stringify([childId, first.runId, first.executionId]);
|
|
237
|
+
const stored = group.some(([, item]) => item.kind === 'terminal')
|
|
238
|
+
? this.#results.get(resultKey)
|
|
239
|
+
: undefined;
|
|
240
|
+
const result = stored && {
|
|
241
|
+
...(stored.summary === undefined ? {} : { summary: stored.summary }),
|
|
242
|
+
...(stored.summaryTruncated ? { summaryTruncated: true } : {}),
|
|
243
|
+
...(stored.turnId ? { turnId: stored.turnId } : {}),
|
|
244
|
+
changedFiles: [...stored.changedFiles],
|
|
245
|
+
...(stored.changedFilesTruncated ? { changedFilesTruncated: true } : {})
|
|
246
|
+
};
|
|
247
|
+
const resultRevision = stored?.revision;
|
|
248
|
+
return {
|
|
249
|
+
childId,
|
|
250
|
+
runId: first.runId,
|
|
251
|
+
executionId: first.executionId,
|
|
252
|
+
firstRevision: Math.min(...group.map(([, item]) => item.revision)),
|
|
253
|
+
...(result === undefined ? {} : { result }),
|
|
254
|
+
updates: group.map(([, item]) => ({
|
|
255
|
+
kind: item.kind,
|
|
256
|
+
value: item.value,
|
|
257
|
+
truncated: item.truncated,
|
|
258
|
+
...(item.final ? { final: true } : {})
|
|
259
|
+
})),
|
|
260
|
+
acknowledge: () => {
|
|
261
|
+
let terminalAcknowledged = false;
|
|
262
|
+
for (const [key, item] of group) {
|
|
263
|
+
if (this.#items.get(key)?.revision !== item.revision)
|
|
264
|
+
continue;
|
|
265
|
+
this.#items.delete(key);
|
|
266
|
+
if (item.kind === 'terminal')
|
|
267
|
+
terminalAcknowledged = true;
|
|
268
|
+
}
|
|
269
|
+
if (terminalAcknowledged && this.#results.get(resultKey)?.revision === resultRevision)
|
|
270
|
+
this.#results.delete(resultKey);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
captureBeforeSend(childId) {
|
|
275
|
+
const captured = [...this.#items].filter(([, item]) => item.childId === childId);
|
|
276
|
+
const results = new Map(captured
|
|
277
|
+
.filter(([, item]) => item.kind === 'terminal')
|
|
278
|
+
.map(([, item]) => {
|
|
279
|
+
const key = JSON.stringify([item.childId, item.runId, item.executionId]);
|
|
280
|
+
return [key, this.#results.get(key)?.revision];
|
|
281
|
+
}));
|
|
282
|
+
return {
|
|
283
|
+
acknowledge: () => {
|
|
284
|
+
for (const [key, item] of captured) {
|
|
285
|
+
if (this.#items.get(key)?.revision !== item.revision)
|
|
286
|
+
continue;
|
|
287
|
+
this.#items.delete(key);
|
|
288
|
+
if (item.kind === 'terminal') {
|
|
289
|
+
const resultKey = JSON.stringify([item.childId, item.runId, item.executionId]);
|
|
290
|
+
if (this.#results.get(resultKey)?.revision === results.get(resultKey))
|
|
291
|
+
this.#results.delete(resultKey);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
close() {
|
|
298
|
+
if (this.#closed)
|
|
299
|
+
return;
|
|
300
|
+
this.#closed = true;
|
|
301
|
+
this.#items.clear();
|
|
302
|
+
this.#results.clear();
|
|
303
|
+
this.#listeners.clear();
|
|
304
|
+
this.#wakeListeners.clear();
|
|
305
|
+
this.#targets.clear();
|
|
306
|
+
this.detach();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function boundedUtf8(value, maximum) {
|
|
310
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
311
|
+
if (bytes.length <= maximum)
|
|
312
|
+
return { value, truncated: false };
|
|
313
|
+
let end = maximum;
|
|
314
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80)
|
|
315
|
+
end--;
|
|
316
|
+
return { value: bytes.subarray(0, end).toString('utf8'), truncated: true };
|
|
317
|
+
}
|
|
318
|
+
function boundedObservation(input) {
|
|
319
|
+
const data = {
|
|
320
|
+
...input,
|
|
321
|
+
...(input.changedFiles ? { changedFiles: [...input.changedFiles] } : {}),
|
|
322
|
+
updates: input.updates.map((update) => ({ ...update }))
|
|
323
|
+
};
|
|
324
|
+
let updatesTruncated = false;
|
|
325
|
+
const render = () => `<mar_subagent_update>\n${JSON.stringify({
|
|
326
|
+
...data,
|
|
327
|
+
...(updatesTruncated ? { updatesTruncated: true } : {})
|
|
328
|
+
}).replace(/[<>&]/g, (character) => character === '<' ? '\\u003c' : character === '>' ? '\\u003e' : '\\u0026')}\n</mar_subagent_update>`;
|
|
329
|
+
const tooLarge = () => Buffer.byteLength(render(), 'utf8') > 32768;
|
|
330
|
+
while (tooLarge() && data.changedFiles?.length) {
|
|
331
|
+
data.changedFiles.pop();
|
|
332
|
+
data.changedFilesTruncated = true;
|
|
333
|
+
}
|
|
334
|
+
const shorten = (value, set) => {
|
|
335
|
+
let low = 0;
|
|
336
|
+
let high = Buffer.byteLength(value, 'utf8');
|
|
337
|
+
while (low < high) {
|
|
338
|
+
const middle = Math.ceil((low + high) / 2);
|
|
339
|
+
set(boundedUtf8(value, middle).value);
|
|
340
|
+
if (tooLarge())
|
|
341
|
+
high = middle - 1;
|
|
342
|
+
else
|
|
343
|
+
low = middle;
|
|
344
|
+
}
|
|
345
|
+
set(boundedUtf8(value, low).value);
|
|
346
|
+
};
|
|
347
|
+
if (tooLarge() && data.summary !== undefined) {
|
|
348
|
+
const summary = data.summary;
|
|
349
|
+
data.summaryTruncated = true;
|
|
350
|
+
shorten(summary, (value) => {
|
|
351
|
+
data.summary = value;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const update of data.updates) {
|
|
355
|
+
if (!tooLarge())
|
|
356
|
+
break;
|
|
357
|
+
const value = update.value;
|
|
358
|
+
update.truncated = true;
|
|
359
|
+
shorten(value, (text) => {
|
|
360
|
+
update.value = text;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
while (tooLarge() && data.updates.length) {
|
|
364
|
+
data.updates.pop();
|
|
365
|
+
updatesTruncated = true;
|
|
366
|
+
}
|
|
367
|
+
if (tooLarge()) {
|
|
368
|
+
const fallback = {
|
|
369
|
+
agentId: boundedUtf8(data.agentId, 512).value,
|
|
370
|
+
runId: boundedUtf8(data.runId, 512).value,
|
|
371
|
+
executionId: boundedUtf8(data.executionId, 512).value,
|
|
372
|
+
detailsTruncated: true
|
|
373
|
+
};
|
|
374
|
+
return `<mar_subagent_update>\n${JSON.stringify(fallback).replace(/[<>&]/g, (character) => character === '<' ? '\\u003c' : character === '>' ? '\\u003e' : '\\u0026')}\n</mar_subagent_update>`;
|
|
375
|
+
}
|
|
376
|
+
return render();
|
|
377
|
+
}
|