@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.
Files changed (51) hide show
  1. package/dist/connector.js +176 -21
  2. package/dist/database.js +35 -11
  3. package/dist/main.js +12 -1
  4. package/dist/marctl-guidance.js +18 -27
  5. package/dist/migrations/v022.js +58 -0
  6. package/dist/runner/abstract-runner.js +14 -0
  7. package/dist/runner/claude/tool-normalizer.js +6 -1
  8. package/dist/runner/codex/conversation-parser.js +23 -4
  9. package/dist/runner/codex/managed-run-controller.js +4 -2
  10. package/dist/runner/codex/tool-normalizer.js +9 -0
  11. package/dist/runner/execution-presentation.js +56 -0
  12. package/dist/runner/mar-agent/agent-network-extension.js +168 -0
  13. package/dist/runner/mar-agent/agent-network-wait-output.js +72 -0
  14. package/dist/runner/mar-agent/conversation-normalizer.js +232 -118
  15. package/dist/runner/mar-agent/managed-run-controller.js +5 -0
  16. package/dist/runner/mar-agent/marctl-skill.js +3 -3
  17. package/dist/runner/mar-agent/runtime-model-factory.js +1 -0
  18. package/dist/runner/mar-agent/session-runtime.js +34 -1
  19. package/dist/runner/mar-agent-runner.js +36 -0
  20. package/dist/runner/opencode/tool-normalizer.js +6 -1
  21. package/dist/runtime-state.js +57 -0
  22. package/dist/service/agent-network-bindings.js +483 -0
  23. package/dist/service/agent-network-goal.js +19 -0
  24. package/dist/service/agent-network-invoke.js +179 -0
  25. package/dist/service/agent-network-observations.js +377 -0
  26. package/dist/service/agent-network-service.js +315 -0
  27. package/dist/service/agent-network-wait.js +130 -0
  28. package/dist/service/composer-message-input.js +9 -2
  29. package/dist/service/continuous-run-coordinator.js +24 -1
  30. package/dist/service/goal-continuation-coordinator.js +12 -2
  31. package/dist/service/marctl-caller.js +80 -0
  32. package/dist/service/marctl-execution-credential-control.js +65 -0
  33. package/dist/service/model-usage-service.js +9 -0
  34. package/dist/service/node-control-message-service.js +10 -0
  35. package/dist/service/node-request-service.js +52 -7
  36. package/dist/service/node-upgrade-coordinator.js +79 -8
  37. package/dist/service/node-upgrade-credential-control.js +57 -0
  38. package/dist/service/node-upgrade-reporter.js +108 -0
  39. package/dist/service/run-event-service.js +8 -0
  40. package/dist/service/run-workbench-service.js +11 -0
  41. package/dist/service/supervised-node-child.js +153 -0
  42. package/dist/service/supervisor-upgrade-handoff.js +104 -0
  43. package/dist/session/native-session-projection-service.js +69 -6
  44. package/dist/session/operation-admission.js +7 -0
  45. package/dist/session/runtime/conversation-projection.js +25 -3
  46. package/dist/session/session-catalog-service.js +156 -32
  47. package/dist/session/session-lifecycle-service.js +68 -42
  48. package/dist/session/session-message-service.js +16 -6
  49. package/dist/supervisor.js +209 -20
  50. package/dist/terminal.js +41 -21
  51. package/package.json +5 -5
@@ -122,6 +122,7 @@ export class MarAgentRunner extends AbstractRunner {
122
122
  id: model.id,
123
123
  label: model.name,
124
124
  supportedEfforts: [...model.reasoningEfforts],
125
+ subagentModel: model.subagentModel === true,
125
126
  defaultEffort: resolveMarAgentDefaultReasoningEffort(model.defaultReasoningEffort, model.reasoningEfforts),
126
127
  ...(index === 0 ? { isDefault: true } : {})
127
128
  })),
@@ -195,6 +196,16 @@ export class MarAgentRunner extends AbstractRunner {
195
196
  ]
196
197
  };
197
198
  }
199
+ childModels() {
200
+ const eligible = new Set(this.#models
201
+ .filter((model) => model.enabled && model.conversationModel !== false && model.subagentModel === true)
202
+ .map((model) => model.id));
203
+ return this.profileForValidation().models.filter((model) => eligible.has(model.id));
204
+ }
205
+ assertChildModel(modelId) {
206
+ if (!this.childModels().some((model) => model.id === modelId))
207
+ throw new Error('MAR_AGENT_SUBAGENT_MODEL_UNAVAILABLE');
208
+ }
198
209
  supportsConfiguration(configuration) {
199
210
  if (configuration.runner !== this.name)
200
211
  return false;
@@ -289,6 +300,9 @@ export class MarAgentRunner extends AbstractRunner {
289
300
  runner: this.name,
290
301
  externalSessionId: session.id,
291
302
  cwd: session.workspace,
303
+ ...(session.source?.parentSessionId === undefined
304
+ ? {}
305
+ : { parentSessionId: session.source.parentSessionId }),
292
306
  lastActivityAt: Date.parse(session.lastActivityAt)
293
307
  })
294
308
  ]
@@ -311,6 +325,9 @@ export class MarAgentRunner extends AbstractRunner {
311
325
  runner: this.name,
312
326
  externalSessionId: session.id,
313
327
  cwd: session.workspace,
328
+ ...(session.source?.parentSessionId === undefined
329
+ ? {}
330
+ : { parentSessionId: session.source.parentSessionId }),
314
331
  ...title,
315
332
  lastActivityAt: Date.parse(session.lastActivityAt)
316
333
  }),
@@ -406,6 +423,25 @@ export class MarAgentRunner extends AbstractRunner {
406
423
  await catalog.close().catch(() => undefined);
407
424
  }
408
425
  }
426
+ supportsNativeBatchRemoval() {
427
+ return true;
428
+ }
429
+ async removeNativeMembers(sessions) {
430
+ const ids = sessions.map((session) => {
431
+ if (session.runner !== this.name || session.externalSessionId === null)
432
+ throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
433
+ return session.externalSessionId;
434
+ });
435
+ for (const id of ids)
436
+ await this.#releaseSessionResources?.(id);
437
+ const catalog = await openAgentSessionCatalog({ homeDirectory: this.homeDirectory });
438
+ try {
439
+ await catalog.deleteMembers(ids);
440
+ }
441
+ finally {
442
+ await catalog.close().catch(() => undefined);
443
+ }
444
+ }
409
445
  async readManagedRunnerTitle(session, nativeSessionId) {
410
446
  if (session.runner !== this.name || session.externalSessionId !== nativeSessionId)
411
447
  return undefined;
@@ -1,4 +1,5 @@
1
1
  import { compactRunnerText, compactRunnerValue } from '../normalization.js';
2
+ import { publicOutputSnapshot, shellExecution } from '../execution-presentation.js';
2
3
  export function normalizeOpenCodeTool(toolName, state) {
3
4
  const status = state?.status === 'completed'
4
5
  ? 'COMPLETED'
@@ -23,7 +24,11 @@ export function normalizeOpenCodeTool(toolName, state) {
23
24
  outputRef: null,
24
25
  exitCode: typeof state?.exitCode === 'number' ? state.exitCode : null,
25
26
  durationMs: null,
26
- truncated: false
27
+ truncated: typeof state?.output === 'string' && state.output.length > 10_000,
28
+ ...(typeof state?.output === 'string'
29
+ ? { output: publicOutputSnapshot(state.output) }
30
+ : {}),
31
+ toolPresentation: shellExecution(typeof state?.output === 'string' ? state.output : undefined)
27
32
  }
28
33
  };
29
34
  return {
@@ -872,6 +872,63 @@ export class NodeRuntimeState {
872
872
  return executionId;
873
873
  return undefined;
874
874
  }
875
+ /** Read the identity and status of one Execution without following a later turn. */
876
+ executionSnapshot(executionId) {
877
+ const execution = this.executions.get(executionId);
878
+ if (execution !== undefined) {
879
+ const run = this.getRun(execution.runId);
880
+ return run === undefined
881
+ ? undefined
882
+ : {
883
+ executionId,
884
+ runId: run.id,
885
+ sessionId: run.sessionId,
886
+ status: execution.status
887
+ };
888
+ }
889
+ const retained = this.retainedExecutionTurns.get(executionId);
890
+ if (retained === undefined)
891
+ return undefined;
892
+ const turn = this.turns.get(retained.sessionId)?.find((value) => value.id === retained.turnId);
893
+ return turn?.runId == null
894
+ ? undefined
895
+ : {
896
+ executionId,
897
+ runId: turn.runId,
898
+ sessionId: retained.sessionId,
899
+ status: turn.status
900
+ };
901
+ }
902
+ latestRunExecution(runId) {
903
+ const turn = this.conversationTurnForRun(runId);
904
+ if (turn === undefined)
905
+ return undefined;
906
+ for (const execution of this.executions.values())
907
+ if (execution.runId === runId && execution.turnId === turn.id)
908
+ return this.executionSnapshot(execution.id);
909
+ for (const [executionId, retained] of this.retainedExecutionTurns)
910
+ if (retained.sessionId === turn.sessionId && retained.turnId === turn.id)
911
+ return this.executionSnapshot(executionId);
912
+ return undefined;
913
+ }
914
+ /** Resolve the current or most recent retained Execution of a Session. */
915
+ latestSessionExecution(sessionId) {
916
+ const active = this.activeSessionRun(sessionId);
917
+ if (active !== undefined)
918
+ return this.latestRunExecution(active.id);
919
+ const turns = this.turns.get(sessionId) ?? [];
920
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
921
+ const turn = turns[index];
922
+ const live = [...this.executions.values()].find((value) => value.turnId === turn.id);
923
+ if (live !== undefined)
924
+ return this.executionSnapshot(live.id);
925
+ for (const [executionId, retained] of this.retainedExecutionTurns) {
926
+ if (retained.sessionId === sessionId && retained.turnId === turn.id)
927
+ return this.executionSnapshot(executionId);
928
+ }
929
+ }
930
+ return undefined;
931
+ }
875
932
  createExecution(runId) {
876
933
  const canonicalRunId = this.canonicalRunId(runId);
877
934
  const turn = this.currentTurnForRun(canonicalRunId);
@@ -0,0 +1,483 @@
1
+ import { auditConversationReceiptSchema, marAgentRuntimeModelConfigurationSchema } from '@myagentroam/protocol';
2
+ import { AgentNetworkService } from './agent-network-service.js';
3
+ import { isPlainRecord } from '../util/records.js';
4
+ import { safeErrorCode } from '../util/safe-error.js';
5
+ import { withSessionMessageOrigin } from './composer-message-input.js';
6
+ import { SESSION_ADMISSION_SIGNAL } from '../session/operation-admission.js';
7
+ import { assertSessionControlOwner } from './run-ownership-policy.js';
8
+ const approvedCalls = new WeakMap();
9
+ const terminalCalls = new WeakMap();
10
+ /** Bind a terminal actor only after the Node request boundary has authenticated its caller. */
11
+ export function actorForAuthenticatedTerminal(actor, workspaceId) {
12
+ const authenticated = Object.freeze({ ...actor });
13
+ terminalCalls.set(authenticated, { workspaceId });
14
+ return authenticated;
15
+ }
16
+ /** Bind a Host-approved invocation to its exact target and domain operations. */
17
+ export function actorForApprovedAgentCall(actor, context, agentIds, operations) {
18
+ if (context.permission?.decision !== 'approved')
19
+ return actor;
20
+ const approved = Object.freeze({ ...actor });
21
+ approvedCalls.set(approved, { agentIds: [...agentIds], operations: [...operations] });
22
+ return approved;
23
+ }
24
+ import { sessionPage } from '../session/pagination.js';
25
+ import { marctlSessionSummary, marctlSessionDetail } from './session-recent-activity.js';
26
+ /** Bind the collaboration use case to the existing Session, Run and Goal owners. */
27
+ export function createNodeAgentNetwork(options) {
28
+ async function actorSession(actor) {
29
+ if (!Number.isSafeInteger(actor.requestUserId) || (actor.requestUserId ?? 0) <= 0)
30
+ throw new Error('USER_AUTHORIZATION_REQUIRED');
31
+ const parent = options.runtime.getAgentSession(actor.sessionId) ??
32
+ (await options.projection.resolve(actor.sessionId, true));
33
+ if (!parent)
34
+ throw new Error('SESSION_NOT_FOUND');
35
+ if (actor.runId !== undefined) {
36
+ const run = options.runtime.getRun(actor.runId);
37
+ if (!run ||
38
+ run.sessionId !== parent.id ||
39
+ options.runtime.activeSessionRun(parent.id)?.id !== run.id ||
40
+ (actor.executionId !== undefined &&
41
+ options.runtime.currentExecutionId(run.id) !== actor.executionId) ||
42
+ run.initiatedByUserId !== actor.requestUserId)
43
+ throw new Error('AGENT_ACTOR_INVALID');
44
+ }
45
+ return parent;
46
+ }
47
+ async function target(actor, id) {
48
+ const terminal = terminalCalls.get(actor);
49
+ const parent = terminal ? undefined : await actorSession(actor);
50
+ const workspaceId = terminal?.workspaceId ?? parent?.workspaceId;
51
+ const session = options.runtime.getAgentSession(id) ?? (await options.projection.resolve(id, true));
52
+ if (!session || session.workspaceId !== workspaceId || session.id === parent?.id)
53
+ throw new Error('SESSION_NOT_FOUND');
54
+ return session;
55
+ }
56
+ function origin(parent) {
57
+ return {
58
+ sessionId: parent.id,
59
+ name: parent.customTitle?.trim() || parent.runnerTitle?.trim() || parent.id
60
+ };
61
+ }
62
+ function childEnvironment(actor) {
63
+ // Retained only inside Node; dispatch exchanges its delegation before creating the child process.
64
+ return actor.secretEnvironment ?? {};
65
+ }
66
+ function executionInput(actor, runner, model, requireSubagent = true) {
67
+ const result = {
68
+ ...actor.runnerConfiguration,
69
+ secretEnvironment: childEnvironment(actor),
70
+ ...(actor.personalInstructions === undefined
71
+ ? {}
72
+ : { personalInstructions: actor.personalInstructions }),
73
+ ...(actor.requestUserId === undefined ? {} : { __requestUserId: actor.requestUserId }),
74
+ ...(actor.usageGrant === undefined ? {} : { __usageGrant: actor.usageGrant })
75
+ };
76
+ if (runner === 'mar-agent') {
77
+ const candidates = [
78
+ result.marAgentModel,
79
+ ...(Array.isArray(result.marAgentModels) ? result.marAgentModels : [])
80
+ ];
81
+ const selected = candidates.flatMap((value) => {
82
+ const parsed = marAgentRuntimeModelConfigurationSchema.safeParse(value);
83
+ return parsed.success && parsed.data.id === model ? [parsed.data] : [];
84
+ })[0];
85
+ if (!selected ||
86
+ !selected.enabled ||
87
+ !selected.conversationModel ||
88
+ (requireSubagent && !selected.subagentModel))
89
+ throw new Error('MAR_AGENT_SUBAGENT_MODEL_UNAVAILABLE');
90
+ result.marAgentModel = selected;
91
+ }
92
+ return result;
93
+ }
94
+ async function validateApproval(actor, session, input) {
95
+ if (!terminalCalls.has(actor)) {
96
+ const parent = await actorSession(actor);
97
+ if (session.parentSessionId !== parent.id)
98
+ throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
99
+ }
100
+ assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
101
+ const approval = options.runtime.getApproval(input.approvalId);
102
+ if (!approval)
103
+ throw new Error('APPROVAL_NOT_FOUND');
104
+ const runId = options.runtime.canonicalRunId(approval.runId);
105
+ const run = options.runtime.getRun(runId);
106
+ const active = options.runtime.activeSessionRun(session.id);
107
+ if (!run ||
108
+ run.sessionId !== session.id ||
109
+ active?.id !== runId ||
110
+ (approval.runId !== runId && approval.runId !== options.runtime.currentExecutionId(runId)))
111
+ throw new Error('APPROVAL_NOT_FOUND');
112
+ if (approval.status === 'EXPIRED')
113
+ throw new Error('APPROVAL_EXPIRED');
114
+ if (approval.status !== 'PENDING')
115
+ throw new Error('APPROVAL_ALREADY_DECIDED');
116
+ }
117
+ return new AgentNetworkService({
118
+ observations: options.observations,
119
+ target,
120
+ authorize: async (actor, session, operation) => {
121
+ assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
122
+ if (terminalCalls.has(actor))
123
+ return;
124
+ const source = await actorSession(actor);
125
+ if (session.parentSessionId === source.id)
126
+ return;
127
+ if (source.parentSessionId === session.id && operation === 'send')
128
+ return;
129
+ const approved = approvedCalls.get(actor);
130
+ if (approved?.agentIds.includes(session.id) && approved.operations.includes(operation))
131
+ return;
132
+ throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
133
+ },
134
+ children: async (actor) => {
135
+ if (terminalCalls.has(actor))
136
+ throw new Error('AGENT_TARGET_REQUIRED');
137
+ const parent = await actorSession(actor);
138
+ const records = options.database()?.listChildSessionRecords(parent.workspaceId, parent.id) ?? [];
139
+ const resolved = await Promise.all(records.map((record) => options.runtime.getAgentSession(record.id) ??
140
+ options.projection.resolve(record.id, true)));
141
+ return resolved.filter((session) => session !== undefined);
142
+ },
143
+ observe: (actor, session) => options.ensureObservations(actor).observe(session.id),
144
+ isUpward: async (actor, session) => !terminalCalls.has(actor) && (await actorSession(actor)).parentSessionId === session.id,
145
+ reportUpward: async (actor, session, input, signal) => {
146
+ const child = await actorSession(actor);
147
+ const runId = actor.runId;
148
+ const executionId = actor.executionId;
149
+ if (!runId ||
150
+ !executionId ||
151
+ options.runtime.currentExecutionId(runId) !== executionId ||
152
+ child.parentSessionId !== session.id)
153
+ throw new Error('AGENT_ACTOR_NOT_ACTIVE');
154
+ signal?.throwIfAborted();
155
+ const parent = options.observations.get(session.id);
156
+ if (!parent?.report({
157
+ childId: child.id,
158
+ runId,
159
+ executionId,
160
+ value: withSessionMessageOrigin(input.content, origin(child), session.parentSessionId)
161
+ }))
162
+ throw new Error('AGENT_PARENT_NOT_WARM');
163
+ return { delivery: 'reported', clientMessageId: input.clientMessageId };
164
+ },
165
+ freezeExecution: (session) => {
166
+ const snapshot = options.runtime.latestSessionExecution(session.id);
167
+ const run = options.runtime.activeSessionRun(session.id);
168
+ const runId = snapshot?.runId ?? run?.id;
169
+ if (!runId)
170
+ return undefined;
171
+ let executionId = snapshot?.executionId;
172
+ const currentId = () => {
173
+ executionId ??= options.runtime.latestRunExecution(runId)?.executionId;
174
+ return executionId;
175
+ };
176
+ return {
177
+ agentId: session.id,
178
+ runId,
179
+ get executionId() {
180
+ return currentId();
181
+ },
182
+ read: () => {
183
+ const id = currentId();
184
+ return id === undefined
185
+ ? (options.runtime.getRun(runId) ?? { status: 'UNKNOWN' })
186
+ : (options.runtime.executionSnapshot(id) ?? { status: 'UNKNOWN' });
187
+ }
188
+ };
189
+ },
190
+ start: async (actor, input, signal) => {
191
+ const terminal = terminalCalls.get(actor);
192
+ const parent = terminal
193
+ ? input.parentSessionId === undefined
194
+ ? undefined
195
+ : await target(actor, input.parentSessionId)
196
+ : await actorSession(actor);
197
+ if (!terminal && input.parentSessionId !== undefined && input.parentSessionId !== parent?.id)
198
+ throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
199
+ signal?.throwIfAborted();
200
+ options.ensureObservations(actor);
201
+ const runner = input.runner ?? parent?.runner;
202
+ if (!runner)
203
+ throw new Error('RUNNER_REQUIRED');
204
+ const runnerAdapter = options.runners.product(runner);
205
+ const database = options.database();
206
+ const workspaceId = terminal?.workspaceId ?? parent?.workspaceId;
207
+ const workspace = workspaceId === undefined ? undefined : database?.getWorkspace(workspaceId);
208
+ if (!runnerAdapter || !database || !workspace)
209
+ throw new Error('SESSION_INVALID');
210
+ const model = input.model ??
211
+ database.getRunnerDefaults().find((entry) => entry.runner === runner)?.model ??
212
+ runnerAdapter.defaultConfiguration(workspace, actor.secretEnvironment ?? {}).model;
213
+ const request = {
214
+ ...executionInput(actor, runner, model, parent !== undefined),
215
+ workspaceId: workspace.id,
216
+ ...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
217
+ ...(parent ? { parentSessionId: parent.id } : {}),
218
+ runner,
219
+ cwd: parent?.cwd ?? workspace.path,
220
+ content: input.content,
221
+ clientMessageId: input.clientMessageId,
222
+ ...(!terminal && parent ? { __marctlOrigin: origin(parent) } : {}),
223
+ ...(input.title === undefined ? {} : { title: input.title }),
224
+ ...(model ? { model } : {}),
225
+ ...(input.effort === undefined ? {} : { effort: input.effort }),
226
+ ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
227
+ ...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
228
+ };
229
+ signal?.throwIfAborted();
230
+ const result = await options.withGrant(actor, () => input.goal.kind === 'set'
231
+ ? options.lifecycle.startAgentWithGoal(request, (session, executionContext) => options.coordinator.start({
232
+ session,
233
+ executionContext,
234
+ objective: input.goal.kind === 'set' ? input.goal.objective : '',
235
+ secretEnvironment: childEnvironment(actor),
236
+ messageContent: input.content,
237
+ ...(!terminal && parent ? { messageOrigin: origin(parent) } : {}),
238
+ clientMessageId: input.clientMessageId,
239
+ ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
240
+ ...(input.attachmentIds === undefined
241
+ ? {}
242
+ : { attachmentIds: input.attachmentIds }),
243
+ ...(actor.requestUserId === undefined
244
+ ? {}
245
+ : { initiatedByUserId: actor.requestUserId }),
246
+ ...(actor.usageGrant === undefined ? {} : { usageGrant: actor.usageGrant }),
247
+ ...(actor.personalInstructions === undefined
248
+ ? {}
249
+ : { personalInstructions: actor.personalInstructions })
250
+ }))
251
+ : options.lifecycle.operations()['session.start'](request));
252
+ if (!isPlainRecord(result) ||
253
+ !isPlainRecord(result.session) ||
254
+ typeof result.session.id !== 'string')
255
+ throw new Error('SESSION_START_INVALID_RESULT');
256
+ const session = await target(actor, result.session.id);
257
+ return {
258
+ session,
259
+ result: { ...publicReceipt(result), clientMessageId: input.clientMessageId }
260
+ };
261
+ },
262
+ setGoal: async (actor, session, change, message, signal) => {
263
+ const parent = terminalCalls.has(actor) ? undefined : await actorSession(actor);
264
+ signal?.throwIfAborted();
265
+ if (change.kind === 'clear')
266
+ return publicReceipt(options.coordinator.clear(session));
267
+ const current = await options.resume.resolveForMessage(session.id);
268
+ const executionContext = options.runners
269
+ .require(current.runner)
270
+ .prepareExecutionContext(executionInput(actor, current.runner, current.model, current.parentSessionId != null));
271
+ signal?.throwIfAborted();
272
+ const result = await options.withGrant(actor, () => options.coordinator.start({
273
+ session: current,
274
+ objective: change.objective,
275
+ executionContext,
276
+ secretEnvironment: childEnvironment(actor),
277
+ ...(actor.requestUserId === undefined ? {} : { initiatedByUserId: actor.requestUserId }),
278
+ ...(actor.personalInstructions === undefined
279
+ ? {}
280
+ : { personalInstructions: actor.personalInstructions }),
281
+ ...(actor.usageGrant === undefined ? {} : { usageGrant: actor.usageGrant }),
282
+ preserveActiveRun: true,
283
+ ...(parent ? { messageOrigin: origin(parent) } : {}),
284
+ ...(message === undefined
285
+ ? {}
286
+ : {
287
+ messageContent: message.content,
288
+ clientMessageId: message.clientMessageId,
289
+ messageIntent: message.deliveryIntent,
290
+ ...(message.attachments === undefined ? {} : { attachments: message.attachments }),
291
+ ...(message.attachmentIds === undefined
292
+ ? {}
293
+ : { attachmentIds: message.attachmentIds })
294
+ })
295
+ }));
296
+ return publicReceipt(result);
297
+ },
298
+ message: async (actor, session, input, signal) => {
299
+ const parent = terminalCalls.has(actor) ? undefined : await actorSession(actor);
300
+ signal?.throwIfAborted();
301
+ const result = await options.withGrant(actor, () => options.messages.operations()['session.message']({
302
+ ...executionInput(actor, session.runner, session.model, session.parentSessionId != null),
303
+ ...input,
304
+ ...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
305
+ sessionId: session.id,
306
+ ...(parent ? { __marctlOrigin: origin(parent) } : {})
307
+ }));
308
+ return publicReceipt(result);
309
+ },
310
+ validateApproval,
311
+ approval: async (actor, session, input, signal) => {
312
+ await validateApproval(actor, session, input);
313
+ signal?.throwIfAborted();
314
+ const approval = options.runtime.getApproval(input.approvalId);
315
+ const runId = options.runtime.canonicalRunId(approval.runId);
316
+ const decision = input.decision === 'approve' ? 'APPROVED' : 'REJECTED';
317
+ try {
318
+ options.runs.operations()['run.approval.decide']({
319
+ runId,
320
+ approvalId: input.approvalId,
321
+ decision,
322
+ __requestUserId: actor.requestUserId
323
+ });
324
+ return {
325
+ status: 'applied',
326
+ result: { approvalId: input.approvalId, decision: input.decision }
327
+ };
328
+ }
329
+ catch (error) {
330
+ if (approval.status === 'PENDING' &&
331
+ options.runtime.getApproval(input.approvalId)?.status === decision)
332
+ return {
333
+ status: 'partial',
334
+ code: safeErrorCode(error, 'APPROVAL_DECISION_FAILED'),
335
+ result: { approvalId: input.approvalId, decision: input.decision }
336
+ };
337
+ throw error;
338
+ }
339
+ },
340
+ stop: (actor, session, executionId) => publicReceipt(options.runs.stopAgentExecution({
341
+ sessionId: session.id,
342
+ executionId,
343
+ ...(actor.requestUserId === undefined ? {} : { requestUserId: actor.requestUserId })
344
+ })),
345
+ goal: (sessionId) => publicGoal(options.goals.get(sessionId)),
346
+ list: async (actor, input) => {
347
+ const terminal = terminalCalls.get(actor);
348
+ const source = terminal ? undefined : await actorSession(actor);
349
+ const workspaceId = terminal?.workspaceId ?? source?.workspaceId;
350
+ if (!workspaceId)
351
+ throw new Error('SESSION_INVALID');
352
+ const current = source ? marctlSessionSummary(options.runtime, source) : null;
353
+ const scope = input.scope ?? (terminal ? 'workspace' : 'children');
354
+ if (scope === 'profile') {
355
+ const runner = input.runner ?? source?.runner;
356
+ if (!runner)
357
+ throw new Error('RUNNER_REQUIRED');
358
+ const profiles = await options.profiles(actor.secretEnvironment ?? {}, actor.runnerConfiguration?.marAgentModels);
359
+ const profile = profiles.find((item) => item.runner === runner);
360
+ if (!profile)
361
+ throw new Error('RUNNER_PROFILE_NOT_FOUND');
362
+ const models = profile.runner !== 'mar-agent'
363
+ ? profile.models
364
+ : profile.models.filter((model) => {
365
+ const configurations = [
366
+ actor.runnerConfiguration?.marAgentModel,
367
+ ...(Array.isArray(actor.runnerConfiguration?.marAgentModels)
368
+ ? actor.runnerConfiguration.marAgentModels
369
+ : [])
370
+ ];
371
+ return configurations.some((value) => isPlainRecord(value) &&
372
+ value.id === model.id &&
373
+ value.enabled === true &&
374
+ value.conversationModel === true &&
375
+ value.subagentModel === true);
376
+ });
377
+ return { current, runner: profile.runner, models, capabilities: profile.capabilities };
378
+ }
379
+ if (scope === 'workspace') {
380
+ const result = await options.listSessions({
381
+ workspaceId,
382
+ view: 'marctl',
383
+ registeredOnly: true,
384
+ ...(input.query === undefined ? {} : { query: input.query }),
385
+ ...(input.parent === undefined ? {} : { parentSessionId: input.parent }),
386
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
387
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor })
388
+ });
389
+ if (!isPlainRecord(result))
390
+ throw new Error('SESSION_LIST_INVALID_RESULT');
391
+ return { ...result, current };
392
+ }
393
+ if (scope !== 'children' && scope !== 'descendants')
394
+ throw new Error('AGENT_LIST_INVALID');
395
+ const root = input.agentId === undefined || input.agentId === source?.id
396
+ ? source
397
+ : await target(actor, input.agentId);
398
+ if (!root)
399
+ throw new Error('AGENT_TARGET_REQUIRED');
400
+ const database = options.database();
401
+ if (!database)
402
+ throw new Error('DATABASE_UNAVAILABLE');
403
+ const records = scope === 'children'
404
+ ? database.listChildSessionRecords(root.workspaceId, root.id)
405
+ : database.sessionTreeRecords(root.workspaceId, root.id).slice(1);
406
+ const sessions = (await Promise.all(records.map((record) => options.runtime.getAgentSession(record.id) ??
407
+ options.projection.resolve(record.id, true))))
408
+ .filter((session) => session !== undefined)
409
+ .filter((session) => !input.query ||
410
+ (session.customTitle ?? session.runnerTitle ?? '')
411
+ .normalize('NFKC')
412
+ .toLocaleLowerCase()
413
+ .includes(input.query.normalize('NFKC').toLocaleLowerCase()));
414
+ const page = sessionPage(sessions, { limit: input.limit, cursor: input.cursor });
415
+ return {
416
+ current,
417
+ sessions: page.sessions.map((session) => marctlSessionSummary(options.runtime, session)),
418
+ nextCursor: page.nextCursor,
419
+ truncated: page.nextCursor !== null
420
+ };
421
+ },
422
+ view: async (actor, input) => {
423
+ const source = terminalCalls.has(actor) ? undefined : await actorSession(actor);
424
+ const session = input.agentId === source?.id ? source : await target(actor, input.agentId);
425
+ assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
426
+ return {
427
+ session: marctlSessionDetail(options.runtime, session, options.runtime.sessionQueue(session.id).itemIds.length),
428
+ goal: publicGoal(options.goals.get(session.id)) ?? null,
429
+ ...(input.includeHistory
430
+ ? {
431
+ history: await options.history({
432
+ sessionId: session.id,
433
+ workspaceId: session.workspaceId,
434
+ registeredOnly: true,
435
+ limit: input.limit,
436
+ cursor: input.cursor
437
+ })
438
+ }
439
+ : {})
440
+ };
441
+ }
442
+ });
443
+ }
444
+ function publicGoal(goal) {
445
+ return goal === undefined
446
+ ? undefined
447
+ : {
448
+ id: goal.id,
449
+ objective: goal.objective,
450
+ status: goal.status,
451
+ revision: goal.revision,
452
+ startedAt: goal.startedAt,
453
+ turns: goal.turns
454
+ };
455
+ }
456
+ /** Project receipts without leaking Session internals or execution credentials. */
457
+ function publicReceipt(value) {
458
+ if (!isPlainRecord(value))
459
+ return {};
460
+ const result = {};
461
+ const audit = auditConversationReceiptSchema.safeParse(value.auditReceipt);
462
+ if (audit.success)
463
+ result.auditReceipt = audit.data;
464
+ for (const key of ['delivery', 'queueItemId', 'idempotent', 'cleared', 'goalAccessChanged'])
465
+ if (typeof value[key] === 'string' || typeof value[key] === 'boolean')
466
+ result[key] = value[key];
467
+ if (isPlainRecord(value.run) && typeof value.run.id === 'string')
468
+ result.run = { id: value.run.id, status: value.run.status };
469
+ if (isPlainRecord(value.turn) && typeof value.turn.id === 'string')
470
+ result.turn = { id: value.turn.id, status: value.turn.status };
471
+ if (isPlainRecord(value.goal)) {
472
+ const goal = value.goal;
473
+ result.goal = {
474
+ id: goal.id,
475
+ objective: goal.objective,
476
+ status: goal.status,
477
+ revision: goal.revision
478
+ };
479
+ }
480
+ if (isPlainRecord(value.goalExit))
481
+ result.goalExit = value.goalExit;
482
+ return result;
483
+ }
@@ -0,0 +1,19 @@
1
+ import { isPlainRecord } from '../util/records.js';
2
+ /** Shared Goal input contract for native tools and CLI. */
3
+ export function parseAgentGoal(input) {
4
+ if (input === undefined || input === null)
5
+ return { kind: 'unchanged' };
6
+ if (!isPlainRecord(input))
7
+ throw new Error('AGENT_GOAL_INVALID');
8
+ const keys = Object.keys(input);
9
+ if (keys.length !== 1)
10
+ throw new Error('AGENT_GOAL_INVALID');
11
+ if (keys[0] === 'clear' && input.clear === true)
12
+ return { kind: 'clear' };
13
+ if (keys[0] === 'objective' && typeof input.objective === 'string') {
14
+ const objective = input.objective.trim();
15
+ if (objective.length > 0 && objective.length <= 20_000)
16
+ return { kind: 'set', objective };
17
+ }
18
+ throw new Error('AGENT_GOAL_INVALID');
19
+ }