@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,315 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isPlainRecord } from '../util/records.js';
|
|
3
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
4
|
+
import { parseAgentGoal } from './agent-network-goal.js';
|
|
5
|
+
import { validateAgentWaitOptions, waitForAgentTargets } from './agent-network-wait.js';
|
|
6
|
+
/** Product collaboration delegates execution and Goal ownership to existing Node services. */
|
|
7
|
+
export class AgentNetworkService {
|
|
8
|
+
options;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
list(actor, input = {}) {
|
|
13
|
+
return this.options.list(actor, input);
|
|
14
|
+
}
|
|
15
|
+
view(actor, input) {
|
|
16
|
+
return this.options.view(actor, input);
|
|
17
|
+
}
|
|
18
|
+
async start(actor, input, signal) {
|
|
19
|
+
signal?.throwIfAborted();
|
|
20
|
+
const goal = parseAgentGoal(input.goal);
|
|
21
|
+
const content = input.content ?? '';
|
|
22
|
+
if (typeof content !== 'string' ||
|
|
23
|
+
(!content.trim() && !input.attachments?.length && !input.attachmentIds?.length) ||
|
|
24
|
+
(input.attachments !== undefined && !Array.isArray(input.attachments)) ||
|
|
25
|
+
(input.attachmentIds !== undefined &&
|
|
26
|
+
(!Array.isArray(input.attachmentIds) ||
|
|
27
|
+
input.attachmentIds.some((id) => typeof id !== 'string' || !id))) ||
|
|
28
|
+
(!!input.attachments?.length && !!input.attachmentIds?.length) ||
|
|
29
|
+
!validOptionalString(input.runner) ||
|
|
30
|
+
!validOptionalString(input.model) ||
|
|
31
|
+
!validOptionalString(input.effort) ||
|
|
32
|
+
!validOptionalString(input.title) ||
|
|
33
|
+
!validOptionalString(input.parentSessionId) ||
|
|
34
|
+
!validOptionalString(input.clientMessageId))
|
|
35
|
+
throw new Error('AGENT_START_INVALID');
|
|
36
|
+
const { goal: _goal, ...start } = input;
|
|
37
|
+
const created = await this.options.start(actor, {
|
|
38
|
+
...start,
|
|
39
|
+
content,
|
|
40
|
+
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
41
|
+
goal
|
|
42
|
+
}, signal);
|
|
43
|
+
try {
|
|
44
|
+
this.options.observe(actor, created.session);
|
|
45
|
+
return { agentId: created.session.id, start: created.result };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
agentId: created.session.id,
|
|
50
|
+
start: created.result,
|
|
51
|
+
observation: { status: 'failed', code: safeErrorCode(error, 'AGENT_OBSERVATION_FAILED') }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async send(actor, input, signal) {
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
const goal = parseAgentGoal(input.goal);
|
|
58
|
+
const content = input.content ?? '';
|
|
59
|
+
if (typeof input.agentId !== 'string' ||
|
|
60
|
+
!input.agentId ||
|
|
61
|
+
typeof content !== 'string' ||
|
|
62
|
+
(!content.trim() &&
|
|
63
|
+
!input.attachments?.length &&
|
|
64
|
+
!input.attachmentIds?.length &&
|
|
65
|
+
goal.kind === 'unchanged' &&
|
|
66
|
+
input.approval === undefined) ||
|
|
67
|
+
(input.attachments !== undefined && !Array.isArray(input.attachments)) ||
|
|
68
|
+
(input.attachmentIds !== undefined &&
|
|
69
|
+
(!Array.isArray(input.attachmentIds) ||
|
|
70
|
+
input.attachmentIds.some((id) => typeof id !== 'string' || !id))) ||
|
|
71
|
+
(!!input.attachments?.length && !!input.attachmentIds?.length) ||
|
|
72
|
+
!validOptionalString(input.clientMessageId) ||
|
|
73
|
+
(input.approval !== undefined &&
|
|
74
|
+
(!isPlainRecord(input.approval) ||
|
|
75
|
+
Object.keys(input.approval).some((key) => key !== 'approvalId' && key !== 'decision') ||
|
|
76
|
+
typeof input.approval.approvalId !== 'string' ||
|
|
77
|
+
!input.approval.approvalId ||
|
|
78
|
+
!['approve', 'reject'].includes(input.approval.decision))))
|
|
79
|
+
throw new Error('AGENT_SEND_INVALID');
|
|
80
|
+
if (input.deliveryIntent !== undefined &&
|
|
81
|
+
!['SEND', 'QUEUE', 'REPLACE_CURRENT'].includes(input.deliveryIntent))
|
|
82
|
+
throw new Error('AGENT_SEND_INVALID');
|
|
83
|
+
const hasMessage = !!content.trim() || !!input.attachments?.length || !!input.attachmentIds?.length;
|
|
84
|
+
const session = await this.options.target(actor, input.agentId);
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
if (hasMessage)
|
|
87
|
+
await this.options.authorize(actor, session, 'send');
|
|
88
|
+
if (goal.kind !== 'unchanged')
|
|
89
|
+
await this.options.authorize(actor, session, 'goal');
|
|
90
|
+
if (input.approval !== undefined)
|
|
91
|
+
await this.options.validateApproval(actor, session, input.approval);
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
if (hasMessage && (await this.options.isUpward(actor, session))) {
|
|
94
|
+
signal?.throwIfAborted();
|
|
95
|
+
if (goal.kind !== 'unchanged' ||
|
|
96
|
+
input.approval !== undefined ||
|
|
97
|
+
input.deliveryIntent === 'REPLACE_CURRENT' ||
|
|
98
|
+
input.deliveryIntent === 'QUEUE' ||
|
|
99
|
+
input.attachments?.length ||
|
|
100
|
+
input.attachmentIds?.length)
|
|
101
|
+
throw new Error('AGENT_UPWARD_REPORT_INVALID');
|
|
102
|
+
const result = await this.options.reportUpward(actor, session, {
|
|
103
|
+
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
104
|
+
content
|
|
105
|
+
}, signal);
|
|
106
|
+
return { agentId: session.id, message: { status: 'accepted', result } };
|
|
107
|
+
}
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
this.options.observe(actor, session);
|
|
110
|
+
const approvalResult = input.approval === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: await this.options.approval(actor, session, input.approval, signal);
|
|
113
|
+
const approvalReceipt = input.approval === undefined ? {} : { approval: approvalResult };
|
|
114
|
+
if (approvalResult?.status === 'partial')
|
|
115
|
+
return {
|
|
116
|
+
agentId: session.id,
|
|
117
|
+
...approvalReceipt,
|
|
118
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'not_attempted' } }),
|
|
119
|
+
message: { status: 'not_attempted' }
|
|
120
|
+
};
|
|
121
|
+
if (signal?.aborted && approvalResult !== undefined)
|
|
122
|
+
return {
|
|
123
|
+
agentId: session.id,
|
|
124
|
+
...approvalReceipt,
|
|
125
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'not_attempted' } }),
|
|
126
|
+
message: { status: 'not_attempted' }
|
|
127
|
+
};
|
|
128
|
+
signal?.throwIfAborted();
|
|
129
|
+
const captured = this.options.observations.get(actor.sessionId)?.captureBeforeSend(session.id);
|
|
130
|
+
const message = hasMessage
|
|
131
|
+
? {
|
|
132
|
+
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
133
|
+
content,
|
|
134
|
+
deliveryIntent: input.deliveryIntent ?? 'SEND',
|
|
135
|
+
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
136
|
+
...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
139
|
+
let goalResult;
|
|
140
|
+
if (goal.kind !== 'unchanged') {
|
|
141
|
+
const previous = this.options.goal(session.id);
|
|
142
|
+
try {
|
|
143
|
+
goalResult = await this.options.setGoal(actor, session, goal, goal.kind === 'set' ? message : undefined, signal);
|
|
144
|
+
if (goal.kind === 'set' && message)
|
|
145
|
+
captured?.acknowledge();
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const current = this.options.goal(session.id);
|
|
149
|
+
if (sameGoal(current, previous) && input.approval === undefined)
|
|
150
|
+
throw error;
|
|
151
|
+
return {
|
|
152
|
+
agentId: session.id,
|
|
153
|
+
...approvalReceipt,
|
|
154
|
+
goal: {
|
|
155
|
+
status: 'failed',
|
|
156
|
+
code: safeErrorCode(error, 'AGENT_GOAL_FAILED'),
|
|
157
|
+
current: current ?? null
|
|
158
|
+
},
|
|
159
|
+
message: { status: goal.kind === 'set' && message ? 'unknown' : 'not_attempted' }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!message || goal.kind === 'set')
|
|
164
|
+
return {
|
|
165
|
+
agentId: session.id,
|
|
166
|
+
...approvalReceipt,
|
|
167
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'applied', result: goalResult } }),
|
|
168
|
+
message: message ? { status: 'accepted', result: goalResult } : null
|
|
169
|
+
};
|
|
170
|
+
if (signal?.aborted && (goal.kind !== 'unchanged' || approvalResult !== undefined))
|
|
171
|
+
return {
|
|
172
|
+
agentId: session.id,
|
|
173
|
+
...approvalReceipt,
|
|
174
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'applied', result: goalResult } }),
|
|
175
|
+
message: { status: 'not_attempted' }
|
|
176
|
+
};
|
|
177
|
+
signal?.throwIfAborted();
|
|
178
|
+
try {
|
|
179
|
+
const result = await this.options.message(actor, session, message, signal);
|
|
180
|
+
captured?.acknowledge();
|
|
181
|
+
return {
|
|
182
|
+
agentId: session.id,
|
|
183
|
+
...approvalReceipt,
|
|
184
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'applied', result: goalResult } }),
|
|
185
|
+
message: { status: 'accepted', result }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (signal?.aborted && error === signal.reason) {
|
|
190
|
+
if (goal.kind === 'unchanged' && approvalResult === undefined)
|
|
191
|
+
throw error;
|
|
192
|
+
return {
|
|
193
|
+
agentId: session.id,
|
|
194
|
+
...approvalReceipt,
|
|
195
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'applied', result: goalResult } }),
|
|
196
|
+
message: { status: 'not_attempted' }
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
agentId: session.id,
|
|
201
|
+
...approvalReceipt,
|
|
202
|
+
...(goal.kind === 'unchanged' ? {} : { goal: { status: 'applied', result: goalResult } }),
|
|
203
|
+
message: { status: 'unknown', code: safeErrorCode(error, 'AGENT_SEND_FAILED') }
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async stop(actor, input) {
|
|
208
|
+
if (typeof input.agentId !== 'string' ||
|
|
209
|
+
!input.agentId ||
|
|
210
|
+
typeof input.executionId !== 'string' ||
|
|
211
|
+
!input.executionId)
|
|
212
|
+
throw new Error('AGENT_STOP_INVALID');
|
|
213
|
+
const session = await this.options.target(actor, input.agentId);
|
|
214
|
+
await this.options.authorize(actor, session, 'stop');
|
|
215
|
+
const goalBefore = this.options.goal(session.id);
|
|
216
|
+
try {
|
|
217
|
+
const result = this.options.stop(actor, session, input.executionId);
|
|
218
|
+
return {
|
|
219
|
+
agentId: session.id,
|
|
220
|
+
executionId: input.executionId,
|
|
221
|
+
cancel: { status: 'accepted', result },
|
|
222
|
+
goal: {
|
|
223
|
+
status: goalBefore != null && this.options.goal(session.id) == null ? 'cleared' : 'unchanged'
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const code = safeErrorCode(error, 'AGENT_STOP_FAILED');
|
|
229
|
+
if (code === 'RUN_EXECUTION_STALE' || code === 'RUN_OWNED_BY_ANOTHER_USER')
|
|
230
|
+
throw error;
|
|
231
|
+
const current = this.options.goal(session.id);
|
|
232
|
+
return {
|
|
233
|
+
agentId: session.id,
|
|
234
|
+
executionId: input.executionId,
|
|
235
|
+
cancel: { status: 'unknown', code },
|
|
236
|
+
goal: { status: goalBefore != null && current == null ? 'cleared' : 'unchanged' }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async wait(actor, input) {
|
|
241
|
+
validateAgentWaitOptions(input);
|
|
242
|
+
if ((input.mode !== undefined && input.agentIds === undefined) ||
|
|
243
|
+
(input.agentId !== undefined && input.agentIds !== undefined) ||
|
|
244
|
+
(input.agentId !== undefined && (typeof input.agentId !== 'string' || !input.agentId)) ||
|
|
245
|
+
(input.agentIds !== undefined &&
|
|
246
|
+
(!Array.isArray(input.agentIds) ||
|
|
247
|
+
input.agentIds.length === 0 ||
|
|
248
|
+
input.agentIds.length > 8 ||
|
|
249
|
+
input.agentIds.some((id) => typeof id !== 'string' || !id) ||
|
|
250
|
+
new Set(input.agentIds).size !== input.agentIds.length)))
|
|
251
|
+
throw new Error('AGENT_WAIT_INVALID');
|
|
252
|
+
input.signal?.throwIfAborted();
|
|
253
|
+
const ids = input.agentId === undefined ? input.agentIds : [input.agentId];
|
|
254
|
+
let sessions;
|
|
255
|
+
if (ids)
|
|
256
|
+
sessions = await Promise.all(ids.map((id) => this.options.target(actor, id)));
|
|
257
|
+
else
|
|
258
|
+
sessions = await this.options.children(actor);
|
|
259
|
+
await Promise.all(sessions.map((session) => this.options.authorize(actor, session, 'wait')));
|
|
260
|
+
input.signal?.throwIfAborted();
|
|
261
|
+
let targets = sessions.flatMap((session) => {
|
|
262
|
+
const target = this.options.freezeExecution(session);
|
|
263
|
+
return target === undefined ? [] : [target];
|
|
264
|
+
});
|
|
265
|
+
if (ids && targets.length !== sessions.length)
|
|
266
|
+
throw new Error('AGENT_EXECUTION_NOT_FOUND');
|
|
267
|
+
if (!ids) {
|
|
268
|
+
const active = targets.filter((target) => !['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(target.read().status));
|
|
269
|
+
if (active.length) {
|
|
270
|
+
const creation = new Map(sessions.map((session) => [session.id, session.createdAt]));
|
|
271
|
+
targets = active.sort((a, b) => (creation.get(b.agentId) ?? 0) - (creation.get(a.agentId) ?? 0));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const withExecution = new Set(targets.map((target) => target.agentId));
|
|
275
|
+
const latest = [...sessions]
|
|
276
|
+
.filter((session) => withExecution.has(session.id))
|
|
277
|
+
.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
278
|
+
targets = targets.filter((target) => target.agentId === latest?.id);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!targets.length)
|
|
282
|
+
throw new Error('AGENT_EXECUTION_NOT_FOUND');
|
|
283
|
+
const selected = new Set(targets.map((target) => target.agentId));
|
|
284
|
+
for (const session of sessions)
|
|
285
|
+
if (selected.has(session.id))
|
|
286
|
+
this.options.observe(actor, session);
|
|
287
|
+
const observations = this.options.observations.get(actor.observationId ?? actor.sessionId);
|
|
288
|
+
if (!observations)
|
|
289
|
+
throw new Error('AGENT_OBSERVATION_UNAVAILABLE');
|
|
290
|
+
return waitForAgentTargets({
|
|
291
|
+
targets,
|
|
292
|
+
observations,
|
|
293
|
+
single: input.agentIds === undefined,
|
|
294
|
+
waitMs: input.waitMs ?? 30_000,
|
|
295
|
+
...(input.mode === undefined ? {} : { mode: input.mode }),
|
|
296
|
+
...(input.waitFor === undefined ? {} : { waitFor: input.waitFor }),
|
|
297
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
298
|
+
...(input.interrupt === undefined ? {} : { interrupt: input.interrupt })
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function sameGoal(left, right) {
|
|
303
|
+
if (left === right)
|
|
304
|
+
return true;
|
|
305
|
+
return (isPlainRecord(left) &&
|
|
306
|
+
isPlainRecord(right) &&
|
|
307
|
+
typeof left.id === 'string' &&
|
|
308
|
+
left.id === right.id &&
|
|
309
|
+
typeof left.revision === 'number' &&
|
|
310
|
+
left.revision === right.revision &&
|
|
311
|
+
left.status === right.status);
|
|
312
|
+
}
|
|
313
|
+
function validOptionalString(value) {
|
|
314
|
+
return value === undefined || (typeof value === 'string' && value.trim().length > 0);
|
|
315
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { waitForToolCondition } from '@myagentroam/agent/extensions';
|
|
2
|
+
export function validateAgentWaitOptions(input) {
|
|
3
|
+
if ((input.mode !== undefined && input.mode !== 'any' && input.mode !== 'all') ||
|
|
4
|
+
(input.waitFor !== undefined &&
|
|
5
|
+
input.waitFor !== 'message' &&
|
|
6
|
+
input.waitFor !== 'completion') ||
|
|
7
|
+
(input.waitMs !== undefined &&
|
|
8
|
+
(!Number.isInteger(input.waitMs) || input.waitMs < 0 || input.waitMs > 300_000)))
|
|
9
|
+
throw new Error('AGENT_WAIT_INVALID');
|
|
10
|
+
}
|
|
11
|
+
/** One deadline and one notification subscription, with Session-scoped consumption. */
|
|
12
|
+
export async function waitForAgentTargets(input) {
|
|
13
|
+
const { targets, observations } = input;
|
|
14
|
+
validateAgentWaitOptions(input);
|
|
15
|
+
if (!targets.length ||
|
|
16
|
+
(!input.single && targets.length > 8) ||
|
|
17
|
+
new Set(targets.map((target) => target.agentId)).size !== targets.length)
|
|
18
|
+
throw new Error('AGENT_WAIT_INVALID');
|
|
19
|
+
input.signal?.throwIfAborted();
|
|
20
|
+
const waitMs = input.waitMs ?? 30_000;
|
|
21
|
+
const snapshot = waitMs === 0;
|
|
22
|
+
const terminalSnapshots = new Map();
|
|
23
|
+
const read = (target) => {
|
|
24
|
+
const state = target.read();
|
|
25
|
+
const executionId = target.executionId;
|
|
26
|
+
const observation = executionId === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: (observations.capture(target.agentId, executionId) ??
|
|
29
|
+
terminalSnapshots.get(JSON.stringify([target.agentId, target.runId, executionId])));
|
|
30
|
+
const terminal = ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(state.status);
|
|
31
|
+
const message = observation?.updates.some((update) => update.kind === 'message') === true;
|
|
32
|
+
return {
|
|
33
|
+
state,
|
|
34
|
+
executionId,
|
|
35
|
+
observation,
|
|
36
|
+
terminal,
|
|
37
|
+
ready: terminal || (input.waitFor === 'message' && message)
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const satisfied = () => {
|
|
41
|
+
const values = targets.map(read);
|
|
42
|
+
return input.mode === 'all'
|
|
43
|
+
? values.every((value) => value.ready)
|
|
44
|
+
: values.some((value) => value.ready);
|
|
45
|
+
};
|
|
46
|
+
let outcome = 'snapshot';
|
|
47
|
+
if (!snapshot) {
|
|
48
|
+
outcome = await waitForToolCondition({
|
|
49
|
+
ready: satisfied,
|
|
50
|
+
subscribe: (listener) => observations.subscribe((terminal) => {
|
|
51
|
+
if (terminal &&
|
|
52
|
+
targets.some((target) => target.agentId === terminal.childId &&
|
|
53
|
+
target.runId === terminal.runId &&
|
|
54
|
+
target.executionId === terminal.executionId))
|
|
55
|
+
terminalSnapshots.set(JSON.stringify([terminal.childId, terminal.runId, terminal.executionId]), terminal);
|
|
56
|
+
listener();
|
|
57
|
+
}),
|
|
58
|
+
waitMs: Math.max(waitMs, 10_000),
|
|
59
|
+
signal: input.signal,
|
|
60
|
+
interrupt: input.interrupt
|
|
61
|
+
});
|
|
62
|
+
input.signal?.throwIfAborted();
|
|
63
|
+
}
|
|
64
|
+
const values = targets.map((target) => ({ target, ...read(target) }));
|
|
65
|
+
const pendingAgentIds = values
|
|
66
|
+
.filter((value) => !value.ready)
|
|
67
|
+
.map((value) => value.target.agentId);
|
|
68
|
+
const ordered = input.single
|
|
69
|
+
? [...values].sort((a, b) => (a.observation?.firstRevision ?? Infinity) - (b.observation?.firstRevision ?? Infinity))
|
|
70
|
+
: values;
|
|
71
|
+
const selected = input.single
|
|
72
|
+
? [
|
|
73
|
+
ordered.find((value) => value.ready) ??
|
|
74
|
+
ordered.find((value) => (value.observation?.updates.length ?? 0) > 0) ??
|
|
75
|
+
values[0]
|
|
76
|
+
]
|
|
77
|
+
: values;
|
|
78
|
+
const agents = selected.map(({ target, executionId, state, observation, terminal, ready }) => {
|
|
79
|
+
const waitOutcome = snapshot
|
|
80
|
+
? 'snapshot'
|
|
81
|
+
: terminal
|
|
82
|
+
? 'completion'
|
|
83
|
+
: ready
|
|
84
|
+
? 'message'
|
|
85
|
+
: outcome === 'timeout'
|
|
86
|
+
? 'timeout'
|
|
87
|
+
: outcome === 'interrupted'
|
|
88
|
+
? 'interrupted'
|
|
89
|
+
: 'snapshot';
|
|
90
|
+
const include = !snapshot && (ready || outcome === 'timeout' || outcome === 'interrupted');
|
|
91
|
+
const projection = include && terminal ? observation?.result : undefined;
|
|
92
|
+
const updates = include && projection?.summary && !projection.summaryTruncated
|
|
93
|
+
? observation?.updates.filter((update) => !update.final)
|
|
94
|
+
: include
|
|
95
|
+
? observation?.updates
|
|
96
|
+
: undefined;
|
|
97
|
+
if (include)
|
|
98
|
+
observation?.acknowledge();
|
|
99
|
+
return {
|
|
100
|
+
agentId: target.agentId,
|
|
101
|
+
runId: target.runId,
|
|
102
|
+
...(executionId === undefined ? {} : { executionId }),
|
|
103
|
+
status: state.status,
|
|
104
|
+
waitOutcome,
|
|
105
|
+
...projection,
|
|
106
|
+
...(updates?.length ? { updates } : {})
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const ready = values.filter((value) => value.ready);
|
|
110
|
+
const groupOutcome = outcome === 'ready'
|
|
111
|
+
? ready.every((value) => value.terminal)
|
|
112
|
+
? 'completion'
|
|
113
|
+
: 'message'
|
|
114
|
+
: outcome;
|
|
115
|
+
if (input.single)
|
|
116
|
+
return {
|
|
117
|
+
...agents[0],
|
|
118
|
+
waitFor: input.waitFor ?? 'completion',
|
|
119
|
+
...(groupOutcome === 'interrupted' ? { waitOutcome: 'interrupted' } : {}),
|
|
120
|
+
...(groupOutcome === 'interrupted' ? { reason: 'steer' } : {})
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
mode: input.mode ?? 'any',
|
|
124
|
+
waitFor: input.waitFor ?? 'completion',
|
|
125
|
+
waitOutcome: snapshot ? 'snapshot' : groupOutcome,
|
|
126
|
+
...(groupOutcome === 'interrupted' ? { reason: 'steer' } : {}),
|
|
127
|
+
agents,
|
|
128
|
+
pendingAgentIds
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -3,10 +3,17 @@ import { relative, resolve } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { SkillDirectoryService } from './skill-directory-service.js';
|
|
5
5
|
/** Keep the admitted source snapshot beside message and Goal markup. */
|
|
6
|
-
export function withSessionMessageOrigin(input, origin) {
|
|
6
|
+
export function withSessionMessageOrigin(input, origin, parentSessionId) {
|
|
7
7
|
if (origin === undefined)
|
|
8
8
|
return input;
|
|
9
|
-
const
|
|
9
|
+
const parsed = sessionMessageOriginSchema.parse(origin);
|
|
10
|
+
const snapshot = parsed.sessionId === undefined
|
|
11
|
+
? parsed
|
|
12
|
+
: {
|
|
13
|
+
...parsed,
|
|
14
|
+
relation: parsed.sessionId === parentSessionId ? 'parent' : 'other'
|
|
15
|
+
};
|
|
16
|
+
const tag = serializeComposerTag('mar_session_origin', snapshot);
|
|
10
17
|
const parts = parseComposerTags(input);
|
|
11
18
|
const first = parts[0];
|
|
12
19
|
const goal = first?.kind === 'mar_goal_start' || first?.kind === 'mar_goal_continue' ? first : undefined;
|
|
@@ -672,8 +672,30 @@ export class ContinuousRunCoordinator {
|
|
|
672
672
|
const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
|
|
673
673
|
const scheduleMcp = await this.options.scheduleMcp?.(executionInput);
|
|
674
674
|
const notificationMcp = await this.options.notificationMcp?.(executionInput);
|
|
675
|
+
const { MAR_CTL_USER_TOKEN: parentToken, ...runnerEnvironment } = pending.secretEnvironment;
|
|
676
|
+
let executionToken;
|
|
677
|
+
if (parentToken !== undefined) {
|
|
678
|
+
const execution = this.options.runtime.executionSnapshot(executionId);
|
|
679
|
+
if (!this.options.mintMarctlExecutionCredential ||
|
|
680
|
+
!execution ||
|
|
681
|
+
execution.sessionId !== pending.sessionId ||
|
|
682
|
+
execution.runId !== active.id ||
|
|
683
|
+
!Number.isSafeInteger(active.initiatedByUserId))
|
|
684
|
+
throw new Error('MARCTL_EXECUTION_AUTHORIZATION_INVALID');
|
|
685
|
+
executionToken = await this.options.mintMarctlExecutionCredential({
|
|
686
|
+
parentToken,
|
|
687
|
+
workspaceId: pending.workspaceId,
|
|
688
|
+
sessionId: pending.sessionId,
|
|
689
|
+
executionId,
|
|
690
|
+
userId: active.initiatedByUserId
|
|
691
|
+
});
|
|
692
|
+
}
|
|
675
693
|
if (this.suspended || this.options.stopped() || !this.dispatchedExecutions.has(executionId))
|
|
676
694
|
return;
|
|
695
|
+
if (executionToken !== undefined &&
|
|
696
|
+
(this.options.runtime.activeSessionRun(pending.sessionId)?.id !== active.id ||
|
|
697
|
+
this.options.runtime.currentExecutionId(active.id) !== executionId))
|
|
698
|
+
throw new Error('MARCTL_EXECUTION_AUTHORIZATION_INVALID');
|
|
677
699
|
if (!this.goalCurrent(pending)) {
|
|
678
700
|
this.dispatchedExecutions.delete(executionId);
|
|
679
701
|
this.rejectStaleGoal(executionId);
|
|
@@ -701,7 +723,8 @@ export class ContinuousRunCoordinator {
|
|
|
701
723
|
collaborationMode: pending.collaborationMode,
|
|
702
724
|
...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
|
|
703
725
|
secretEnvironment: {
|
|
704
|
-
...
|
|
726
|
+
...runnerEnvironment,
|
|
727
|
+
...(executionToken === undefined ? {} : { MAR_CTL_USER_TOKEN: executionToken }),
|
|
705
728
|
MAR_CTL_NODE_ID: this.options.runtime.getAgentSession(pending.sessionId)?.nodeId ?? '',
|
|
706
729
|
MAR_CTL_WORKSPACE_ID: pending.workspaceId,
|
|
707
730
|
MAR_CTL_SESSION_ID: this.options.runtime.getAgentSession(pending.sessionId)?.id ?? pending.sessionId,
|
|
@@ -179,7 +179,7 @@ export class GoalContinuationCoordinator {
|
|
|
179
179
|
goalAccessChanged: prepared.changed
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
-
async start({ session, objective, initiatedByUserId, suppressNotifications, secretEnvironment, personalInstructions, executionContext, usageGrant }) {
|
|
182
|
+
async start({ session, objective, initiatedByUserId, suppressNotifications, secretEnvironment, personalInstructions, executionContext, usageGrant, preserveActiveRun, messageIntent, messageContent, clientMessageId, messageOrigin, attachments, attachmentIds }) {
|
|
183
183
|
this.invalidateGoalContinuation(session.id);
|
|
184
184
|
const prepared = this.prepareGoalSession(session);
|
|
185
185
|
const goalSession = prepared.session;
|
|
@@ -198,8 +198,18 @@ export class GoalContinuationCoordinator {
|
|
|
198
198
|
kind: 'start',
|
|
199
199
|
content: this.options.goals.prompt(attempt.sessionId, 'start'),
|
|
200
200
|
objective: goal.objective,
|
|
201
|
-
deliveryIntent:
|
|
201
|
+
deliveryIntent: messageIntent ??
|
|
202
|
+
(preserveActiveRun && activeRun !== undefined
|
|
203
|
+
? 'QUEUE'
|
|
204
|
+
: replacing
|
|
205
|
+
? 'REPLACE_CURRENT'
|
|
206
|
+
: 'SEND'),
|
|
202
207
|
goal: goalRef,
|
|
208
|
+
...(messageContent === undefined ? {} : { messageContent }),
|
|
209
|
+
...(clientMessageId === undefined ? {} : { clientMessageId }),
|
|
210
|
+
...(messageOrigin === undefined ? {} : { messageOrigin }),
|
|
211
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
212
|
+
...(attachmentIds === undefined ? {} : { attachmentIds }),
|
|
203
213
|
...(suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
204
214
|
...(usageGrant === undefined ? {} : { usageGrant }),
|
|
205
215
|
...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isPlainRecord } from '../util/records.js';
|
|
2
|
+
import { assertSessionControlOwner } from './run-ownership-policy.js';
|
|
3
|
+
const executionReadOperations = new Set([
|
|
4
|
+
'agent.network',
|
|
5
|
+
'session.cli.context',
|
|
6
|
+
'session.get',
|
|
7
|
+
'session.list',
|
|
8
|
+
'session.graph',
|
|
9
|
+
'session.runs',
|
|
10
|
+
'session.goal.get',
|
|
11
|
+
'session.turns.list',
|
|
12
|
+
'workspace.queue.get',
|
|
13
|
+
'session.schedule.list',
|
|
14
|
+
'session.schedule.get',
|
|
15
|
+
'runner.profiles.list',
|
|
16
|
+
'session.watch',
|
|
17
|
+
'session.unwatch'
|
|
18
|
+
]);
|
|
19
|
+
const executionManagementOperations = new Set([
|
|
20
|
+
'session.metadata.update',
|
|
21
|
+
'session.config.update',
|
|
22
|
+
'session.schedule.agent.save'
|
|
23
|
+
]);
|
|
24
|
+
/** Prevent execution credentials from reaching legacy writes outside their Session tree. */
|
|
25
|
+
export async function authorizeMarctlExecutionOperation(input) {
|
|
26
|
+
if (!isPlainRecord(input.caller))
|
|
27
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
28
|
+
if (input.caller.type === 'terminal')
|
|
29
|
+
return;
|
|
30
|
+
if (input.caller.type !== 'execution' || typeof input.caller.sessionId !== 'string')
|
|
31
|
+
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
32
|
+
if (executionReadOperations.has(input.operation))
|
|
33
|
+
return;
|
|
34
|
+
if (!executionManagementOperations.has(input.operation) ||
|
|
35
|
+
!isPlainRecord(input.data) ||
|
|
36
|
+
typeof input.data.sessionId !== 'string' ||
|
|
37
|
+
typeof input.data.workspaceId !== 'string')
|
|
38
|
+
throw new Error('MARCTL_AGENT_NETWORK_REQUIRED');
|
|
39
|
+
const source = input.runtime.getAgentSession(input.caller.sessionId);
|
|
40
|
+
const target = input.runtime.getAgentSession(input.data.sessionId) ??
|
|
41
|
+
(await input.resolveSession(input.data.sessionId));
|
|
42
|
+
if (!source ||
|
|
43
|
+
!target ||
|
|
44
|
+
source.workspaceId !== input.data.workspaceId ||
|
|
45
|
+
target.workspaceId !== source.workspaceId ||
|
|
46
|
+
(target.id !== source.id && target.parentSessionId !== source.id))
|
|
47
|
+
throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
|
|
48
|
+
assertSessionControlOwner(input.runtime, target.id, input.userId);
|
|
49
|
+
}
|
|
50
|
+
/** Resolve only Server-verified credentials against the current Node Execution owner. */
|
|
51
|
+
export function resolveMarctlCaller(input) {
|
|
52
|
+
if (!isPlainRecord(input.caller))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (input.caller.type === 'terminal')
|
|
55
|
+
return { type: 'terminal' };
|
|
56
|
+
if (input.caller.type !== 'execution' ||
|
|
57
|
+
typeof input.caller.sessionId !== 'string' ||
|
|
58
|
+
typeof input.caller.executionId !== 'string' ||
|
|
59
|
+
!isPlainRecord(input.data) ||
|
|
60
|
+
typeof input.data.workspaceId !== 'string')
|
|
61
|
+
return undefined;
|
|
62
|
+
const session = input.runtime.getAgentSession(input.caller.sessionId);
|
|
63
|
+
const execution = input.runtime.executionSnapshot(input.caller.executionId);
|
|
64
|
+
const prepared = input.coordinator.pending(input.caller.executionId);
|
|
65
|
+
const run = execution && input.runtime.getRun(execution.runId);
|
|
66
|
+
if (!session ||
|
|
67
|
+
!execution ||
|
|
68
|
+
!prepared ||
|
|
69
|
+
!run ||
|
|
70
|
+
session.workspaceId !== input.data.workspaceId ||
|
|
71
|
+
execution.sessionId !== session.id ||
|
|
72
|
+
prepared.sessionId !== session.id ||
|
|
73
|
+
prepared.workspaceId !== session.workspaceId ||
|
|
74
|
+
run.initiatedByUserId !== input.userId ||
|
|
75
|
+
!['STARTING', 'RUNNING'].includes(execution.status) ||
|
|
76
|
+
input.runtime.activeSessionRun(session.id)?.id !== run.id ||
|
|
77
|
+
input.runtime.currentExecutionId(run.id) !== execution.executionId)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { type: 'execution', sessionId: session.id, executionId: execution.executionId };
|
|
80
|
+
}
|