@myagentroam/node 0.11.51 → 0.11.52

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 CHANGED
@@ -194,10 +194,9 @@ export class NodeConnector {
194
194
  await this.sessionMessageService.drain();
195
195
  },
196
196
  closeResume: () => this.externalSessionResumeService.close(),
197
- capture: async () => {
197
+ capture: () => {
198
198
  if (!this.config?.nodeId)
199
199
  return;
200
- await this.marAgentRuntimeModels.waitForPendingCredentials();
201
200
  return this.restartHandoff.capture();
202
201
  },
203
202
  dispose: () => this.disposeRuntime(),
@@ -1102,7 +1101,6 @@ export class NodeConnector {
1102
1101
  goals: this.goalRuntime,
1103
1102
  runs: this.runWorkbenchService,
1104
1103
  observations: this.agentNetworkObservations,
1105
- history: async (input) => this.sessionQueryService.operations()['session.turns.list'](input),
1106
1104
  ensureObservations: (actor) => {
1107
1105
  const existing = this.agentNetworkObservations.get(actor.observationId ?? actor.sessionId);
1108
1106
  if (existing)
@@ -1133,7 +1131,36 @@ export class NodeConnector {
1133
1131
  messages: this.sessionMessageService,
1134
1132
  resolve: (id) => this.externalSessionResumeService.resolveForMessage(id),
1135
1133
  inputState,
1136
- withGrant: (context, work) => this.modelUsage.withGrant(context.usageGrant, work),
1134
+ withExecution: async (session, checkpoint, work) => {
1135
+ if (checkpoint.initiatedByUserId === undefined)
1136
+ throw new Error('RESTART_EXECUTION_USER_UNAVAILABLE');
1137
+ const context = await this.scheduledRunAuthorization.prepare({
1138
+ userId: checkpoint.initiatedByUserId,
1139
+ sessionId: session.id,
1140
+ workspaceId: session.workspaceId,
1141
+ runner: session.runner,
1142
+ modelId: checkpoint.configuration === undefined
1143
+ ? session.modelId
1144
+ : checkpoint.configuration.modelId
1145
+ });
1146
+ return this.gitOperationService.execute('session.message', {
1147
+ sessionId: session.id,
1148
+ workspaceId: session.workspaceId,
1149
+ __requestUserId: checkpoint.initiatedByUserId,
1150
+ ...context
1151
+ }, async (data) => this.modelUsage.withGrant(context.usageGrant, () => work({
1152
+ ...checkpoint,
1153
+ secretEnvironment: data
1154
+ .secretEnvironment,
1155
+ executionContext: this.runners
1156
+ .require(session.runner)
1157
+ .prepareExecutionContext(context),
1158
+ ...(typeof context.personalInstructions === 'string'
1159
+ ? { personalInstructions: context.personalInstructions }
1160
+ : {}),
1161
+ ...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant })
1162
+ })));
1163
+ },
1137
1164
  publishGoal: (session, goal) => this.goalCoordinator.publishGoalState(session, goal),
1138
1165
  goalReference: (sessionId) => this.goalCoordinator.reference(sessionId),
1139
1166
  publishQueue: (session) => this.continuousRunCoordinator.publish(session.id)
@@ -1146,7 +1173,6 @@ export class NodeConnector {
1146
1173
  coordinator: this.continuousRunCoordinator,
1147
1174
  goals: this.goalRuntime,
1148
1175
  goalExecution: (id) => this.goalCoordinator.restartExecution(id),
1149
- executionContext: (id, context, modelId) => this.marAgentRunController.restartExecutionContext(id, context, modelId === undefined ? this.runtime.getAgentSession(id)?.modelId : modelId),
1150
1176
  commandActive: (id, command) => this.commandStates.get(id, command)?.active === true,
1151
1177
  inputState,
1152
1178
  restore: (entry, id) => restorer.restore(entry, id),
@@ -1486,6 +1512,9 @@ export class NodeConnector {
1486
1512
  modelId: configuredSession.modelId
1487
1513
  });
1488
1514
  return {
1515
+ ...(context.gitConfiguration === undefined
1516
+ ? {}
1517
+ : { gitConfiguration: context.gitConfiguration }),
1489
1518
  secretEnvironment: context.secretEnvironment,
1490
1519
  personalInstructions: context.personalInstructions,
1491
1520
  usageGrant: context.usageGrant,
@@ -1493,16 +1522,25 @@ export class NodeConnector {
1493
1522
  };
1494
1523
  },
1495
1524
  submit: async (task, occurrenceId, context) => {
1496
- const result = (await this.sessionMessageService.submitScheduled({
1525
+ const result = (await this.gitOperationService.execute('session.message', {
1526
+ sessionId: task.sessionId,
1527
+ workspaceId: task.workspaceId,
1528
+ __requestUserId: task.ownerUserId,
1529
+ secretEnvironment: context.secretEnvironment,
1530
+ ...(context.gitConfiguration === undefined
1531
+ ? {}
1532
+ : { gitConfiguration: context.gitConfiguration })
1533
+ }, async (data) => this.sessionMessageService.submitScheduled({
1497
1534
  sessionId: task.sessionId,
1498
1535
  occurrenceId,
1499
1536
  objective: task.objective,
1500
1537
  initiatedByUserId: task.ownerUserId,
1501
- secretEnvironment: context.secretEnvironment,
1538
+ secretEnvironment: data
1539
+ .secretEnvironment,
1502
1540
  personalInstructions: context.personalInstructions,
1503
1541
  ...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
1504
1542
  executionContext: context.executionContext
1505
- }));
1543
+ })));
1506
1544
  return { runId: result.run.id };
1507
1545
  },
1508
1546
  sessionTitle: (sessionId) => {
@@ -5,11 +5,13 @@ export function hasMarctlContext(environment) {
5
5
  !!environment['MAR_CTL_NODE_ID'] &&
6
6
  !!environment['MAR_CTL_WORKSPACE_ID']);
7
7
  }
8
- export const marctlManagementDescription = 'Use marctl to manage MAR Agent session settings (model, effort, title, persistent prompts, and schedules), discover authorized MAR Nodes, and execute commands or transfer files on those Nodes. Use native agent_* tools for other Agent lifecycle and collaboration operations.';
8
+ export const marctlManagementDescription = 'Use marctl to manage MAR Agent session settings (model, effort, title, persistent prompts, and schedules), read paginated public history, discover authorized MAR Nodes, and execute commands or transfer files on those Nodes. Use native agent_* tools for other Agent lifecycle and collaboration operations.';
9
+ const marctlHistory = 'Use marctl agents history <id> [--cursor <cursor>] [--limit <1..100>] --json to read public history on demand; agents view returns state, Goal and recent activity only.';
9
10
  const marctlSafety = 'If a request result is unknown, inspect history before retrying. Session IDs and parent relationships do not grant authorization.';
10
11
  export const marctlManagementInstructions = [
11
12
  '## MAR Workspace management',
12
13
  'Use native agent_* tools for Agent discovery, creation, communication, observation, waiting and stopping. Do not switch to marctl when a native operation fails.',
14
+ marctlHistory,
13
15
  'Use marctl agents config <id> --model-id <id> --effort <effort> to change future model settings; agents rename <id> --title <title> changes a title.',
14
16
  'Use agents prompt get/set/clear for persistent instructions; set accepts -m or --message-file. Use agents schedule list/get/create/update for scheduled tasks; update requires --expected-revision.',
15
17
  'marctl nodes lists authorized Nodes; marctl ssh executes remote commands and marctl scp transfers files. Use marctl help agents, nodes, ssh or scp for options.',
@@ -20,12 +22,13 @@ export const marctlInstructions = [
20
22
  '## MAR Workspace CLI',
21
23
  'Run marctl from the shell. Commands use the authenticated Node and Workspace; marctl help agents, nodes, ssh or scp shows options.',
22
24
  'Use agents start --title <title> -m <text> [--runner <runner>] [--model-id <id>] [--effort <effort>] [--goal <objective>]. An Agent must supply a nonblank title; a manual terminal may omit it. The default Runner is the current Session Runner; a terminal without a current Session must specify --runner or an explicit --parent. Start returns without waiting for completion.',
23
- 'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id> --execution <execution-id>, agents list [--scope tree|active], and agents view <id> [--history]. --timeout 0ms takes a snapshot. Stop clears the target Session Goal.',
25
+ 'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id>, agents list [--scope tree|active], and agents view <id>. --timeout 0ms takes a snapshot; positive durations are used as requested. Stop targets the current Session execution and clears its Goal.',
26
+ marctlHistory,
24
27
  'While children run, prefer advancing meaningful, non-overlapping task work rather than duplicating delegated work. Waiting is generally useful once no meaningful local task work remains and a required result is needed to unblock or finish the task.',
25
28
  'Prefer agents send <id> --steer -m <text> for timely communication or course correction. --queue is best reserved for deferred input explicitly requested by the user or recipient: it is consumed after current work and earlier queued inputs finish, and with an active Goal it is not consumed until that Goal exits. Acceptance is not a guarantee of immediate processing.',
26
29
  'An accepted message normally needs no separate acknowledgment. Requests for receipt-only replies and immediate agents view checks solely to confirm receipt are usually unnecessary.',
27
30
  'Send can update a Goal alone with --goal <objective> or --clear-goal; omitted Goal leaves it unchanged. Decide an authorized Agent approval with send <id> --approval <approval-id> --decision approve|reject.',
28
- 'List accepts only --scope tree|active, optional --query <title-fragment> and --limit <1..100> (default 100). With a current Session it defaults to the containing collaboration tree, including idle Sessions; without one it defaults to active Workspace Sessions (running or sleeping), and explicit tree is rejected. Returns currentAgentId (null without a Session), flat sessions with id, parentSessionId, title, runner and status, and truncated; no cursor or pagination. Reuse known IDs directly.',
31
+ 'List accepts only --scope tree|active, optional --query <title-fragment> and --limit <1..100> (default 10). With a current Session it defaults to the containing collaboration tree, including idle Sessions; without one it defaults to active Workspace Sessions (running or sleeping), and explicit tree is rejected. Active Sessions come first, then recent activity descending. Returns currentAgentId (null without a Session), flat sessions with id, parentSessionId, title and status, and truncated; no cursor or pagination. Reuse known IDs directly.',
29
32
  'Only direct-child updates are collected automatically. Sending to a non-child never subscribes; an explicit wait observes it only for that call.',
30
33
  'Use agents config, rename, prompt and schedule for management. marctl nodes, ssh and scp use authorized Nodes.',
31
34
  marctlSafety
@@ -77,9 +77,9 @@ const tools = [
77
77
  },
78
78
  {
79
79
  action: 'stop',
80
- description: 'Stop the specified Agent Execution and clear its Goal.',
81
- properties: { agentId: text, executionId: text },
82
- required: ['agentId', 'executionId']
80
+ description: 'Stop the Agent’s current execution and clear its Goal.',
81
+ properties: { agentId: text },
82
+ required: ['agentId']
83
83
  },
84
84
  {
85
85
  action: 'list',
@@ -95,17 +95,14 @@ const tools = [
95
95
  maxLength: 160,
96
96
  description: 'Optional case-insensitive title fragment within the scope.'
97
97
  },
98
- limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Defaults to 100.' }
98
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Defaults to 10.' }
99
99
  }
100
100
  },
101
101
  {
102
102
  action: 'view',
103
- description: "View an Agent's state, Goal and public history.",
103
+ description: "View an Agent's state, Goal and recent activity.",
104
104
  properties: {
105
- agentId: text,
106
- includeHistory: { type: 'boolean' },
107
- limit: { type: 'integer', minimum: 1, maximum: 100 },
108
- cursor: text
105
+ agentId: text
109
106
  },
110
107
  required: ['agentId']
111
108
  }
@@ -15,7 +15,6 @@ import { MarAgentConversationNormalizer, normalizeMarAgentUsage } from './conver
15
15
  import { parseRunnerImageAttachments } from '../../session/attachments.js';
16
16
  import { runnerErrorProjection } from '../runner-error.js';
17
17
  import { MarAgentRuntimeModelFactory, sdkModelConfigurations, closeImageCredentialProviders } from './runtime-model-factory.js';
18
- import { snapshotRuntimeModel } from './runtime-model-factory.js';
19
18
  import { createImageGenerationExtensions } from './image-generation-extensions.js';
20
19
  import { createWebToolsExtensions } from './web-tools-extensions.js';
21
20
  import { combineExecutionExtensions } from './execution-extensions.js';
@@ -95,43 +94,6 @@ export class MarAgentManagedRunController {
95
94
  releaseSession(sessionId) {
96
95
  return this.#runtimes.releaseSession(sessionId);
97
96
  }
98
- restartExecutionContext(sessionId, context, selectedModelId) {
99
- const runtime = this.#runtimes.findRuntime(sessionId);
100
- const input = marAgentEnvelopeInput(context);
101
- if (runtime === undefined || typeof input?.runtimeScopeId !== 'string')
102
- return context;
103
- const scope = input.runtimeScopeId;
104
- const snapshots = new Map(runtime.models.flatMap((model) => {
105
- const snapshot = snapshotRuntimeModel(model, scope);
106
- return snapshot === undefined ? [] : [[model.id, snapshot]];
107
- }));
108
- if (snapshots.size === 0)
109
- return context;
110
- const refresh = (value) => {
111
- if (value === null || typeof value !== 'object' || !('id' in value))
112
- return value;
113
- return typeof value.id === 'string' ? (snapshots.get(value.id) ?? value) : value;
114
- };
115
- const models = Array.isArray(input.models) ? input.models.map(refresh) : input.models;
116
- const selected = Array.isArray(models)
117
- ? models.find((model) => model !== null &&
118
- typeof model === 'object' &&
119
- 'id' in model &&
120
- model.id === selectedModelId)
121
- : undefined;
122
- const compact = input.compact;
123
- return {
124
- runner: 'mar-agent',
125
- runnerInput: {
126
- ...input,
127
- model: selected ?? refresh(input.model),
128
- models,
129
- compact: compact !== null && typeof compact === 'object' && 'model' in compact
130
- ? { ...compact, model: refresh(compact.model) }
131
- : compact
132
- }
133
- };
134
- }
135
97
  async executeCommand(session, command, input, context) {
136
98
  if (command.id === 'plan') {
137
99
  const current = this.host.commandState(session.id, 'plan');
@@ -21,42 +21,14 @@ class PendingCredentialResolution {
21
21
  this.initialReason = initialReason;
22
22
  }
23
23
  }
24
- const restartConfigurations = new WeakMap();
25
- export function snapshotRuntimeModel(model, scope) {
26
- const state = restartConfigurations.get(model);
27
- if (state === undefined || state.scope !== scope)
28
- return;
29
- const configuration = structuredClone(state.configuration);
30
- for (const credential of [configuration.credential, configuration.imageGeneration?.credential]) {
31
- if (credential?.type !== 'RUNTIME_TICKET')
32
- continue;
33
- const key = `${credential.credentialId}\0${credential.ticket.runtimeScopeId}\0${credential.ticket.purpose ?? 'MODEL'}`;
34
- const current = state.tickets.get(key);
35
- if (current === undefined || current.released)
36
- return;
37
- credential.ticket = structuredClone(current.ticket);
38
- }
39
- return configuration;
40
- }
41
24
  const CREDENTIAL_REFRESH_WINDOW_MS = 60_000;
42
25
  const CREDENTIAL_REFRESH_RETRY_MS = 30_000;
43
26
  export class MarAgentRuntimeModelFactory {
44
- now;
45
27
  control;
46
- pendingCredentials = new Set();
28
+ now;
47
29
  constructor(control, now = Date.now) {
30
+ this.control = control;
48
31
  this.now = now;
49
- this.control = {
50
- resolve: (...args) => {
51
- const pending = control.resolve(...args);
52
- this.pendingCredentials.add(pending);
53
- void pending.then(() => this.pendingCredentials.delete(pending), () => this.pendingCredentials.delete(pending));
54
- return pending;
55
- }
56
- };
57
- }
58
- async waitForPendingCredentials() {
59
- await Promise.allSettled([...this.pendingCredentials]);
60
32
  }
61
33
  create(configurations, context) {
62
34
  const tickets = new Map();
@@ -64,14 +36,6 @@ export class MarAgentRuntimeModelFactory {
64
36
  try {
65
37
  return configurations.map((raw) => {
66
38
  const configuration = marAgentRuntimeModelConfigurationSchema.parse(raw);
67
- const remember = (model) => {
68
- restartConfigurations.set(model, {
69
- configuration,
70
- scope: context.runtimeScopeId,
71
- tickets
72
- });
73
- return model;
74
- };
75
39
  const imageGeneration = configuration.imageGeneration === undefined ||
76
40
  configuration.imageGeneration.credential === undefined
77
41
  ? undefined
@@ -125,10 +89,10 @@ export class MarAgentRuntimeModelFactory {
125
89
  };
126
90
  const { credential } = configuration;
127
91
  if (credential.type === 'INLINE_API_KEY')
128
- return remember({
92
+ return {
129
93
  ...model,
130
94
  apiKey: new SecretValue(credential.value)
131
- });
95
+ };
132
96
  if (credential?.type !== 'RUNTIME_TICKET')
133
97
  throw new AgentError('MAR_AGENT_RUNTIME_MODEL_CREDENTIAL_INVALID', 'The runtime model credential is invalid.');
134
98
  if (context.runtimeScopeId === undefined ||
@@ -136,7 +100,7 @@ export class MarAgentRuntimeModelFactory {
136
100
  !credential.ticket.allowedModelIds.includes(configuration.id))
137
101
  throw new AgentError('MAR_AGENT_RUNTIME_CREDENTIAL_SCOPE_INVALID', 'The runtime credential does not authorize this execution or model.');
138
102
  const provider = runtimeProvider(credential, configuration.id, context, tickets, providers, this.control, this.now);
139
- return remember({
103
+ return {
140
104
  ...model,
141
105
  ...(configuration.imageGeneration !== undefined &&
142
106
  configuration.imageGeneration.credential === undefined
@@ -153,7 +117,7 @@ export class MarAgentRuntimeModelFactory {
153
117
  }
154
118
  : {}),
155
119
  credentialProvider: provider
156
- });
120
+ };
157
121
  });
158
122
  }
159
123
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { auditConversationReceiptSchema, marAgentRuntimeModelConfigurationSchema } from '@myagentroam/protocol';
2
+ import { AgentError } from '@myagentroam/agent';
2
3
  import { resolvedPersonalInstructions } from '../runner/personal-instructions.js';
3
4
  import { AgentNetworkService } from './agent-network-service.js';
4
5
  import { isPlainRecord } from '../util/records.js';
@@ -12,17 +13,17 @@ export function actorForAuthenticatedTerminal(actor, workspaceId) {
12
13
  terminalCalls.set(authenticated, { workspaceId });
13
14
  return authenticated;
14
15
  }
15
- import { marctlSessionDetail } from './session-recent-activity.js';
16
+ import { agentNetworkSessionView } from './session-recent-activity.js';
16
17
  import { listAgentNetworkSessions } from './agent-network-list.js';
17
18
  /** Bind the collaboration use case to the existing Session, Run and Goal owners. */
18
19
  export function createNodeAgentNetwork(options) {
19
20
  async function actorSession(actor) {
20
21
  if (!Number.isSafeInteger(actor.requestUserId) || (actor.requestUserId ?? 0) <= 0)
21
- throw new Error('USER_AUTHORIZATION_REQUIRED');
22
+ throw new AgentError('USER_AUTHORIZATION_REQUIRED', 'An authenticated user is required for agent operations.');
22
23
  const parent = options.runtime.getAgentSession(actor.sessionId) ??
23
24
  (await options.projection.resolve(actor.sessionId, true));
24
25
  if (!parent)
25
- throw new Error('SESSION_NOT_FOUND');
26
+ throw new AgentError('SESSION_NOT_FOUND', 'The calling agent session was not found.');
26
27
  if (actor.runId !== undefined) {
27
28
  const run = options.runtime.getRun(actor.runId);
28
29
  if (!run ||
@@ -31,7 +32,7 @@ export function createNodeAgentNetwork(options) {
31
32
  (actor.executionId !== undefined &&
32
33
  options.runtime.currentExecutionId(run.id) !== actor.executionId) ||
33
34
  run.initiatedByUserId !== actor.requestUserId)
34
- throw new Error('AGENT_ACTOR_INVALID');
35
+ throw new AgentError('AGENT_ACTOR_INVALID', 'The calling agent execution identity is invalid.');
35
36
  }
36
37
  return parent;
37
38
  }
@@ -41,7 +42,7 @@ export function createNodeAgentNetwork(options) {
41
42
  const workspaceId = terminal?.workspaceId ?? parent?.workspaceId;
42
43
  const session = options.runtime.getAgentSession(id) ?? (await options.projection.resolve(id, true));
43
44
  if (!session || session.workspaceId !== workspaceId)
44
- throw new Error('SESSION_NOT_FOUND');
45
+ throw new AgentError('SESSION_NOT_FOUND', 'The target agent session was not found in this Workspace.');
45
46
  return session;
46
47
  }
47
48
  function ancestorDistance(source, targetId) {
@@ -138,7 +139,7 @@ export function createNodeAgentNetwork(options) {
138
139
  },
139
140
  children: async (actor) => {
140
141
  if (terminalCalls.has(actor))
141
- throw new Error('AGENT_TARGET_REQUIRED');
142
+ throw new AgentError('AGENT_TARGET_REQUIRED', 'A terminal caller must specify an agent to wait for.');
142
143
  const parent = await actorSession(actor);
143
144
  const records = options.database()?.listChildSessionRecords(parent.workspaceId, parent.id) ?? [];
144
145
  const resolved = await Promise.all(records.map((record) => options.runtime.getAgentSession(record.id) ??
@@ -179,9 +180,18 @@ export function createNodeAgentNetwork(options) {
179
180
  },
180
181
  read: () => {
181
182
  const id = currentId();
182
- return id === undefined
183
- ? (options.runtime.getRun(runId) ?? { status: 'UNKNOWN' })
184
- : (options.runtime.executionSnapshot(id) ?? { status: 'UNKNOWN' });
183
+ const state = id === undefined
184
+ ? options.runtime.getRun(runId)
185
+ : options.runtime.executionSnapshot(id);
186
+ if (!state)
187
+ throw new AgentError('AGENT_EXECUTION_NOT_FOUND', 'The execution selected for this wait is no longer available.', {
188
+ details: {
189
+ agentId: session.id,
190
+ runId,
191
+ ...(id === undefined ? {} : { executionId: id })
192
+ }
193
+ });
194
+ return state;
185
195
  }
186
196
  };
187
197
  },
@@ -329,11 +339,16 @@ export function createNodeAgentNetwork(options) {
329
339
  throw error;
330
340
  }
331
341
  },
332
- stop: (actor, session, executionId) => publicReceipt(options.runs.stopAgentExecution({
333
- sessionId: session.id,
334
- executionId,
335
- ...(actor.requestUserId === undefined ? {} : { requestUserId: actor.requestUserId })
336
- })),
342
+ stop: (actor, session) => {
343
+ const result = options.runs.stopAgentSession({
344
+ sessionId: session.id,
345
+ ...(actor.requestUserId === undefined ? {} : { requestUserId: actor.requestUserId })
346
+ });
347
+ return {
348
+ ...publicReceipt(result),
349
+ ...(isPlainRecord(result) && result.status === 'IDLE' ? { status: 'IDLE' } : {})
350
+ };
351
+ },
337
352
  goal: (sessionId) => publicGoal(options.goals.get(sessionId)),
338
353
  list: async (actor, input) => {
339
354
  const terminal = terminalCalls.get(actor);
@@ -354,19 +369,8 @@ export function createNodeAgentNetwork(options) {
354
369
  assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
355
370
  await onResolved?.([session]);
356
371
  return {
357
- session: marctlSessionDetail(options.runtime, session, options.runtime.sessionQueue(session.id).itemIds.length),
358
- goal: publicGoal(options.goals.get(session.id)) ?? null,
359
- ...(input.includeHistory
360
- ? {
361
- history: await options.history({
362
- sessionId: session.id,
363
- workspaceId: session.workspaceId,
364
- registeredOnly: true,
365
- limit: input.limit,
366
- cursor: input.cursor
367
- })
368
- }
369
- : {})
372
+ session: agentNetworkSessionView(options.runtime, session, options.runtime.sessionQueue(session.id).itemIds.length),
373
+ goal: publicGoal(options.goals.get(session.id)) ?? null
370
374
  };
371
375
  }
372
376
  });
@@ -1,3 +1,4 @@
1
+ import { AgentError } from '@myagentroam/agent';
1
2
  import { actorForAuthenticatedTerminal } from './agent-network-bindings.js';
2
3
  import { isPlainRecord } from '../util/records.js';
3
4
  import { randomUUID } from 'node:crypto';
@@ -27,7 +28,7 @@ export function executionAgentActor(input) {
27
28
  input.runtime.currentExecutionId(run.id) !== executionId ||
28
29
  !['STARTING', 'RUNNING'].includes(execution.status) ||
29
30
  !Number.isSafeInteger(run.initiatedByUserId))
30
- throw new Error('AGENT_ACTOR_NOT_ACTIVE');
31
+ throw new AgentError('AGENT_ACTOR_NOT_ACTIVE', 'The calling agent execution is no longer active.');
31
32
  const captured = prepared.executionContext.runner === 'mar-agent' &&
32
33
  isPlainRecord(prepared.executionContext.runnerInput)
33
34
  ? prepared.executionContext.runnerInput
@@ -57,7 +58,7 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
57
58
  case 'start':
58
59
  if ((context || actor.executionId) &&
59
60
  (typeof payload.title !== 'string' || !payload.title.trim()))
60
- throw new Error('AGENT_START_INVALID');
61
+ throw new AgentError('AGENT_START_INVALID', 'A nonblank title is required to start an agent.');
61
62
  if (!context &&
62
63
  !actor.executionId &&
63
64
  typeof payload.title === 'string' &&
@@ -52,20 +52,31 @@ export async function listAgentNetworkSessions(source, input, options) {
52
52
  sessions = (await Promise.all(ids.map((id) => known.get(id) ?? options.projection.resolve(id, true)))).filter((session) => session !== undefined && session.workspaceId === options.workspaceId);
53
53
  }
54
54
  else {
55
- sessions = [...known.values()].sort((left, right) => right.lastActivityAt - left.lastActivityAt || left.id.localeCompare(right.id));
55
+ sessions = [...known.values()];
56
56
  }
57
57
  const query = input.query?.trim().normalize('NFKC').toLocaleLowerCase() ?? '';
58
58
  const matches = sessions
59
59
  .map((session) => {
60
- const { id, title, runner, status } = marctlSessionSummary(options.runtime, session);
61
- return { id, parentSessionId: session.parentSessionId ?? null, title, runner, status };
60
+ const { id, title, status } = marctlSessionSummary(options.runtime, session);
61
+ return {
62
+ id,
63
+ parentSessionId: session.parentSessionId ?? null,
64
+ title,
65
+ status,
66
+ lastActivityAt: session.lastActivityAt
67
+ };
62
68
  })
63
69
  .filter((session) => (scope !== 'active' || session.status !== 'IDLE') &&
64
- (!query || session.title.normalize('NFKC').toLocaleLowerCase().includes(query)));
65
- const limit = input.limit ?? 100;
70
+ (!query || session.title.normalize('NFKC').toLocaleLowerCase().includes(query)))
71
+ .sort((left, right) => Number(right.status !== 'IDLE') - Number(left.status !== 'IDLE') ||
72
+ right.lastActivityAt - left.lastActivityAt ||
73
+ left.id.localeCompare(right.id));
74
+ const limit = input.limit ?? 10;
66
75
  return {
67
76
  currentAgentId: source?.id ?? null,
68
- sessions: matches.slice(0, limit),
77
+ sessions: matches
78
+ .slice(0, limit)
79
+ .map(({ lastActivityAt: _lastActivityAt, ...session }) => session),
69
80
  truncated: matches.length > limit
70
81
  };
71
82
  }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { AgentError } from '@myagentroam/agent';
2
3
  import { isPlainRecord } from '../util/records.js';
3
4
  import { safeErrorCode } from '../util/safe-error.js';
4
5
  import { parseAgentGoal } from './agent-network-goal.js';
@@ -12,7 +13,11 @@ export class AgentNetworkService {
12
13
  list(actor, input = {}) {
13
14
  return this.options.list(actor, input);
14
15
  }
15
- view(actor, input, onResolved) {
16
+ async view(actor, input, onResolved) {
17
+ if (typeof input.agentId !== 'string' ||
18
+ !input.agentId.trim() ||
19
+ Object.keys(input).some((key) => key !== 'agentId'))
20
+ throw new AgentError('AGENT_VIEW_INVALID', 'Agent view accepts only a target agent ID.');
16
21
  return this.options.view(actor, input, onResolved);
17
22
  }
18
23
  async start(actor, input, signal, onResolved) {
@@ -30,7 +35,7 @@ export class AgentNetworkService {
30
35
  !validOptionalString(input.title) ||
31
36
  !validOptionalString(input.parentSessionId) ||
32
37
  !validOptionalString(input.clientMessageId))
33
- throw new Error('AGENT_START_INVALID');
38
+ throw new AgentError('AGENT_START_INVALID', 'The agent start input is invalid.');
34
39
  const { goal: _goal, ...start } = input;
35
40
  const created = await this.options.start(actor, {
36
41
  ...start,
@@ -69,10 +74,10 @@ export class AgentNetworkService {
69
74
  typeof input.approval.approvalId !== 'string' ||
70
75
  !input.approval.approvalId ||
71
76
  !['approve', 'reject'].includes(input.approval.decision))))
72
- throw new Error('AGENT_SEND_INVALID');
77
+ throw new AgentError('AGENT_SEND_INVALID', 'The agent send input is invalid.');
73
78
  if (input.deliveryIntent !== undefined &&
74
79
  !['SEND', 'QUEUE', 'REPLACE_CURRENT'].includes(input.deliveryIntent))
75
- throw new Error('AGENT_SEND_INVALID');
80
+ throw new AgentError('AGENT_SEND_INVALID', 'The message delivery intent is invalid.');
76
81
  const hasMessage = !!content.trim();
77
82
  const session = await this.options.target(actor, input.agentId);
78
83
  signal?.throwIfAborted();
@@ -185,20 +190,21 @@ export class AgentNetworkService {
185
190
  }
186
191
  async stop(actor, input, onResolved) {
187
192
  if (typeof input.agentId !== 'string' ||
188
- !input.agentId ||
189
- typeof input.executionId !== 'string' ||
190
- !input.executionId)
191
- throw new Error('AGENT_STOP_INVALID');
193
+ !input.agentId.trim() ||
194
+ Object.keys(input).some((key) => key !== 'agentId'))
195
+ throw new AgentError('AGENT_STOP_INVALID', 'Agent stop accepts only a target agent ID.');
192
196
  const session = await this.options.target(actor, input.agentId);
193
197
  await this.options.authorize(actor, session, 'stop');
194
198
  await onResolved?.([session]);
195
199
  const goalBefore = this.options.goal(session.id);
196
200
  try {
197
- const result = this.options.stop(actor, session, input.executionId);
201
+ const result = this.options.stop(actor, session);
198
202
  return {
199
203
  agentId: session.id,
200
- executionId: input.executionId,
201
- cancel: { status: 'accepted', result },
204
+ cancel: {
205
+ status: isPlainRecord(result) && result.status === 'IDLE' ? 'idle' : 'accepted',
206
+ result
207
+ },
202
208
  goal: {
203
209
  status: goalBefore != null && this.options.goal(session.id) == null ? 'cleared' : 'unchanged'
204
210
  }
@@ -206,12 +212,11 @@ export class AgentNetworkService {
206
212
  }
207
213
  catch (error) {
208
214
  const code = safeErrorCode(error, 'AGENT_STOP_FAILED');
209
- if (code === 'RUN_EXECUTION_STALE' || code === 'RUN_OWNED_BY_ANOTHER_USER')
215
+ if (code === 'RUN_OWNED_BY_ANOTHER_USER')
210
216
  throw error;
211
217
  const current = this.options.goal(session.id);
212
218
  return {
213
219
  agentId: session.id,
214
- executionId: input.executionId,
215
220
  cancel: { status: 'unknown', code },
216
221
  goal: { status: goalBefore != null && current == null ? 'cleared' : 'unchanged' }
217
222
  };
@@ -228,7 +233,7 @@ export class AgentNetworkService {
228
233
  input.agentIds.length > 8 ||
229
234
  input.agentIds.some((id) => typeof id !== 'string' || !id) ||
230
235
  new Set(input.agentIds).size !== input.agentIds.length)))
231
- throw new Error('AGENT_WAIT_INVALID');
236
+ throw new AgentError('AGENT_WAIT_INVALID', 'The agent wait target selection is invalid.');
232
237
  input.signal?.throwIfAborted();
233
238
  const ids = input.agentId === undefined ? input.agentIds : [input.agentId];
234
239
  let sessions;
@@ -242,8 +247,18 @@ export class AgentNetworkService {
242
247
  const target = this.options.freezeExecution(session);
243
248
  return target === undefined ? [] : [target];
244
249
  });
245
- if (ids && targets.length !== sessions.length)
246
- throw new Error('AGENT_EXECUTION_NOT_FOUND');
250
+ if (!targets.length || (ids && targets.length !== sessions.length)) {
251
+ if (!sessions.length)
252
+ throw new AgentError('AGENT_WAIT_NO_TARGETS', 'There are no child agents to wait for.');
253
+ const idle = ids
254
+ ? sessions.filter((session) => !targets.some((target) => target.agentId === session.id))
255
+ : [...sessions].sort((a, b) => b.createdAt - a.createdAt).slice(0, 1);
256
+ const agentIds = idle.map((session) => session.id);
257
+ const message = 'Agent is idle; there is no execution to wait for.';
258
+ throw new AgentError('AGENT_IDLE', input.agentId === undefined
259
+ ? `${message} Idle agent IDs: ${agentIds.join(', ')}.`
260
+ : message, { details: { agentIds, status: 'IDLE' } });
261
+ }
247
262
  if (!ids) {
248
263
  const active = targets.filter((target) => !['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(target.read().status));
249
264
  if (active.length) {
@@ -258,8 +273,6 @@ export class AgentNetworkService {
258
273
  targets = targets.filter((target) => target.agentId === latest?.id);
259
274
  }
260
275
  }
261
- if (!targets.length)
262
- throw new Error('AGENT_EXECUTION_NOT_FOUND');
263
276
  const selected = new Set(targets.map((target) => target.agentId));
264
277
  await onResolved?.(targets.map((target) => sessions.find((session) => session.id === target.agentId)));
265
278
  const directChildren = new Set();
@@ -270,7 +283,7 @@ export class AgentNetworkService {
270
283
  }
271
284
  const parent = this.options.observations.get(actor.observationId ?? actor.sessionId);
272
285
  if (directChildren.size && !parent)
273
- throw new Error('AGENT_OBSERVATION_UNAVAILABLE');
286
+ throw new AgentError('AGENT_OBSERVATION_UNAVAILABLE', 'The agent observation channel is unavailable.');
274
287
  const otherTargets = targets.filter((target) => !directChildren.has(target.agentId));
275
288
  const temporary = otherTargets.length && input.waitMs !== 0
276
289
  ? this.options.observations.watchExecutions(otherTargets)
@@ -1,3 +1,4 @@
1
+ import { AgentError } from '@myagentroam/agent';
1
2
  import { waitForToolCondition } from '@myagentroam/agent/extensions';
2
3
  export function validateAgentWaitOptions(input) {
3
4
  if ((input.mode !== undefined && input.mode !== 'any' && input.mode !== 'all') ||
@@ -6,7 +7,7 @@ export function validateAgentWaitOptions(input) {
6
7
  input.waitFor !== 'completion') ||
7
8
  (input.waitMs !== undefined &&
8
9
  (!Number.isInteger(input.waitMs) || input.waitMs < 0 || input.waitMs > 300_000)))
9
- throw new Error('AGENT_WAIT_INVALID');
10
+ throw new AgentError('AGENT_WAIT_INVALID', 'The agent wait mode, condition or duration is invalid.');
10
11
  }
11
12
  /** One deadline and one notification subscription, with Session-scoped consumption. */
12
13
  export async function waitForAgentTargets(input) {
@@ -15,7 +16,7 @@ export async function waitForAgentTargets(input) {
15
16
  if (!targets.length ||
16
17
  (!input.single && targets.length > 8) ||
17
18
  new Set(targets.map((target) => target.agentId)).size !== targets.length)
18
- throw new Error('AGENT_WAIT_INVALID');
19
+ throw new AgentError('AGENT_WAIT_INVALID', 'The agent wait target selection is invalid.');
19
20
  input.signal?.throwIfAborted();
20
21
  const waitMs = input.waitMs ?? 30_000;
21
22
  const snapshot = waitMs === 0;
@@ -55,7 +56,7 @@ export async function waitForAgentTargets(input) {
55
56
  terminalSnapshots.set(JSON.stringify([terminal.childId, terminal.runId, terminal.executionId]), terminal);
56
57
  listener();
57
58
  }),
58
- waitMs: Math.max(waitMs, 10_000),
59
+ waitMs,
59
60
  signal: input.signal,
60
61
  interrupt: input.interrupt
61
62
  });
@@ -120,6 +120,9 @@ export class GoalContinuationCoordinator {
120
120
  void this.enqueueGoalContinuation({
121
121
  sessionId: retry.sessionId,
122
122
  goal: pending.goal,
123
+ ...(retry.initiatedByUserId === undefined
124
+ ? {}
125
+ : { initiatedByUserId: retry.initiatedByUserId }),
123
126
  ...(pending.usageGrant === undefined ? {} : { usageGrant: pending.usageGrant }),
124
127
  secretEnvironment: pending.secretEnvironment,
125
128
  ...(pending.personalInstructions === undefined
@@ -70,7 +70,7 @@ const sessionSchema = z
70
70
  .strict();
71
71
  export const plannedRestartSchema = z
72
72
  .object({
73
- schemaVersion: z.literal(3),
73
+ schemaVersion: z.literal(4),
74
74
  id: identifier,
75
75
  nodeId: identifier,
76
76
  serverUrl: z.string().min(1),
@@ -198,8 +198,6 @@ export class PlannedRestartHandoff {
198
198
  effort: session.effort,
199
199
  access: session.access
200
200
  };
201
- if (execution !== undefined && this.options.executionContext !== undefined)
202
- execution.executionContext = this.options.executionContext(session.id, execution.executionContext, execution.configuration?.modelId);
203
201
  return execution;
204
202
  };
205
203
  const toMessage = (id, clientMessageId, content, attachments, originatingUser, suppressNotifications, activeInput = false) => ({
@@ -262,7 +260,7 @@ export class PlannedRestartHandoff {
262
260
  });
263
261
  }
264
262
  return parseCapture(plannedRestartSchema, {
265
- schemaVersion: 3,
263
+ schemaVersion: 4,
266
264
  id: randomUUID(),
267
265
  ...this.options.identity(),
268
266
  createdAt: Date.now(),
@@ -1,5 +1,4 @@
1
1
  import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
2
- import { modelUsageGrantSchema } from '@myagentroam/protocol';
3
2
  import { z } from 'zod';
4
3
  export const restartExecutionSchema = z
5
4
  .object({
@@ -11,14 +10,6 @@ export const restartExecutionSchema = z
11
10
  })
12
11
  .strict()
13
12
  .optional(),
14
- executionContext: z
15
- .object({ runner: z.literal('mar-agent'), runnerInput: z.unknown().optional() })
16
- .strict(),
17
- secretEnvironment: z.record(z.string(), z.string()),
18
- personalInstructions: z.string().optional(),
19
- usageGrant: z
20
- .custom((value) => modelUsageGrantSchema.safeParse(value).success)
21
- .optional(),
22
13
  initiatedByUserId: z.number().int().positive().optional()
23
14
  })
24
15
  .strict();
@@ -29,12 +20,6 @@ export function captureRestartExecution(input, initiatedByUserId) {
29
20
  ...(input.modelId === undefined || input.effort === undefined || input.access === undefined
30
21
  ? {}
31
22
  : { configuration: { modelId: input.modelId, effort: input.effort, access: input.access } }),
32
- executionContext: structuredClone(input.executionContext),
33
- secretEnvironment: { ...input.secretEnvironment },
34
- ...(input.personalInstructions === undefined
35
- ? {}
36
- : { personalInstructions: input.personalInstructions }),
37
- ...(input.usageGrant === undefined ? {} : { usageGrant: { ...input.usageGrant } }),
38
23
  ...(initiatedByUserId === undefined ? {} : { initiatedByUserId })
39
24
  });
40
25
  }
@@ -67,8 +67,7 @@ export class RestartSessionRestorer {
67
67
  closeInput: '/goal --clear'
68
68
  });
69
69
  const submit = async (input, content = input.content) => {
70
- const context = execution(input.execution);
71
- await this.options.withGrant(context, () => this.options.messages.restoreQueued({
70
+ await this.options.withExecution(session, execution(input.execution), (context) => this.options.messages.restoreQueued({
72
71
  sessionId: session.id,
73
72
  content,
74
73
  clientMessageId: input.clientMessageId,
@@ -110,8 +109,7 @@ export class RestartSessionRestorer {
110
109
  for (const input of entry.queue)
111
110
  await submit(input);
112
111
  if (entry.goal?.goal.status === 'active') {
113
- const context = execution(entry.execution);
114
- await this.options.withGrant(context, () => this.options.messages.submitGoal({
112
+ await this.options.withExecution(session, execution(entry.execution), (context) => this.options.messages.submitGoal({
115
113
  sessionId: session.id,
116
114
  kind: 'continue',
117
115
  deliveryIntent: 'QUEUE',
@@ -1,4 +1,4 @@
1
- import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema } from '@myagentroam/protocol';
1
+ import { createEnvelope, scheduledRunAuthorizeRequestSchema, executionPrepareResponseSchema, executionPrepareRequestSchema } from '@myagentroam/protocol';
2
2
  export class RunAuthorizationControl {
3
3
  send;
4
4
  #pending = new Map();
@@ -8,9 +8,6 @@ export class RunAuthorizationControl {
8
8
  constructor(send) {
9
9
  this.send = send;
10
10
  }
11
- code(suffix) {
12
- return `SCHEDULE_${suffix}`;
13
- }
14
11
  connected() {
15
12
  this.disconnected();
16
13
  this.#connected = true;
@@ -19,29 +16,38 @@ export class RunAuthorizationControl {
19
16
  this.#connected = false;
20
17
  this.#generation++;
21
18
  for (const pending of [...this.#pending.values()])
22
- pending.reject(new Error(this.code('SERVER_UNAVAILABLE')));
19
+ pending.reject(new Error(`${pending.prefix}_SERVER_UNAVAILABLE`));
23
20
  }
24
21
  close() {
25
22
  this.#closed = true;
26
23
  this.disconnected();
27
24
  }
28
25
  handle(envelope) {
29
- if (envelope.type !== 'schedule.authorize.response')
26
+ if (envelope.type !== 'schedule.authorize.response' &&
27
+ envelope.type !== 'execution.prepare.response')
30
28
  return false;
31
29
  const pending = envelope.replyTo === undefined ? undefined : this.#pending.get(envelope.replyTo);
32
- if (pending === undefined || pending.generation !== this.#generation)
30
+ if (pending === undefined ||
31
+ pending.generation !== this.#generation ||
32
+ pending.responseType !== envelope.type)
33
33
  return true;
34
- const parsed = scheduledRunAuthorizeResponseSchema.safeParse(envelope.payload);
34
+ const parsed = executionPrepareResponseSchema.safeParse(envelope.payload);
35
35
  if (!parsed.success)
36
- pending.reject(new Error(this.code('AUTH_RESPONSE_INVALID')));
36
+ pending.reject(new Error(`${pending.prefix}_AUTH_RESPONSE_INVALID`));
37
37
  else
38
38
  pending.resolve(parsed.data);
39
39
  return true;
40
40
  }
41
41
  authorize(request) {
42
+ return this.request('schedule.authorize', scheduledRunAuthorizeRequestSchema.parse({ ...request, executionResources: 1 }), 'SCHEDULE');
43
+ }
44
+ prepare(request) {
45
+ return this.request('execution.prepare', executionPrepareRequestSchema.parse(request), 'EXECUTION');
46
+ }
47
+ request(type, request, prefix) {
42
48
  if (!this.#connected || this.#closed)
43
- return Promise.reject(new Error(this.code('SERVER_UNAVAILABLE')));
44
- const envelope = createEnvelope('schedule.authorize', scheduledRunAuthorizeRequestSchema.parse(request));
49
+ return Promise.reject(new Error(`${prefix}_SERVER_UNAVAILABLE`));
50
+ const envelope = createEnvelope(type, request);
45
51
  return new Promise((resolve, reject) => {
46
52
  const settle = (action) => {
47
53
  const pending = this.#pending.get(envelope.id);
@@ -51,10 +57,12 @@ export class RunAuthorizationControl {
51
57
  clearTimeout(pending.timer);
52
58
  action();
53
59
  };
54
- const timer = setTimeout(() => settle(() => reject(new Error(this.code('AUTH_TIMEOUT')))), 15_000);
60
+ const timer = setTimeout(() => settle(() => reject(new Error(`${prefix}_AUTH_TIMEOUT`))), 15_000);
55
61
  timer.unref();
56
62
  this.#pending.set(envelope.id, {
57
63
  generation: this.#generation,
64
+ responseType: `${type}.response`,
65
+ prefix,
58
66
  timer,
59
67
  resolve: (response) => settle(() => {
60
68
  if ('error' in response)
@@ -68,7 +76,7 @@ export class RunAuthorizationControl {
68
76
  this.send(envelope);
69
77
  }
70
78
  catch {
71
- settle(() => reject(new Error(this.code('SERVER_UNAVAILABLE'))));
79
+ settle(() => reject(new Error(`${prefix}_SERVER_UNAVAILABLE`)));
72
80
  }
73
81
  });
74
82
  }
@@ -1,10 +1,11 @@
1
+ import { AgentError } from '@myagentroam/agent';
1
2
  export function assertRunOwner(run, requestUserId) {
2
3
  if (Number.isSafeInteger(requestUserId) && run.initiatedByUserId !== requestUserId)
3
- throw new Error('RUN_OWNED_BY_ANOTHER_USER');
4
+ throw new AgentError('RUN_OWNED_BY_ANOTHER_USER', 'The run belongs to another user.');
4
5
  }
5
6
  export function assertQueueItemOwner(item, requestUserId) {
6
7
  if (Number.isSafeInteger(requestUserId) && item.initiatedByUserId !== requestUserId)
7
- throw new Error('RUN_OWNED_BY_ANOTHER_USER');
8
+ throw new AgentError('RUN_OWNED_BY_ANOTHER_USER', 'The queued input belongs to another user.');
8
9
  }
9
10
  export function assertSessionControlOwner(runtime, sessionId, requestUserId) {
10
11
  if (!Number.isSafeInteger(requestUserId))
@@ -3,7 +3,7 @@ import { nodeOperationData } from '../connection/node-operation-data.js';
3
3
  import { isTerminalRunStatus } from '../session/runtime/type-guards.js';
4
4
  import { isPlainRecord } from '../util/records.js';
5
5
  import { isHostedConversationImageRunId } from '../runner/mar-agent/generated-images.js';
6
- import { assertRunOwner } from './run-ownership-policy.js';
6
+ import { assertRunOwner, assertSessionControlOwner } from './run-ownership-policy.js';
7
7
  import { imagePreviews } from './image-preview-service.js';
8
8
  export class RunWorkbenchService {
9
9
  options;
@@ -22,16 +22,25 @@ export class RunWorkbenchService {
22
22
  'run.approval.decide': (data) => this.decideApproval(record(data))
23
23
  };
24
24
  }
25
- /** Atomically validate the target Execution before invoking the existing stop path. */
26
- stopAgentExecution(input) {
25
+ /** Resolve and stop the current Session execution inside its existing control owner. */
26
+ stopAgentSession(input) {
27
27
  const run = this.options.runtime.activeSessionRun(input.sessionId);
28
- if (run === undefined ||
29
- (this.options.runtime.currentExecutionId(run.id) ?? run.id) !== input.executionId)
30
- throw new Error('RUN_EXECUTION_STALE');
31
- return this.cancelOrInterrupt('run.cancel', {
32
- runId: run.id,
33
- ...(input.requestUserId === undefined ? {} : { __requestUserId: input.requestUserId })
34
- });
28
+ if (run !== undefined)
29
+ return this.cancelOrInterrupt('run.cancel', {
30
+ runId: run.id,
31
+ ...(input.requestUserId === undefined ? {} : { __requestUserId: input.requestUserId })
32
+ });
33
+ if (!this.options.runtime.getAgentSession(input.sessionId))
34
+ throw new Error('SESSION_NOT_FOUND');
35
+ assertSessionControlOwner(this.options.runtime, input.sessionId, input.requestUserId);
36
+ let result;
37
+ try {
38
+ this.options.pauseQueue(input.sessionId);
39
+ }
40
+ finally {
41
+ result = this.options.runInterrupted(input.sessionId);
42
+ }
43
+ return { status: 'IDLE', ...result };
35
44
  }
36
45
  async modelResource(input) {
37
46
  const parsed = modelResourceReadInputSchema.safeParse(nodeOperationData(input));
@@ -138,6 +138,23 @@ export function marctlSessionDetail(runtime, session, queuedCount) {
138
138
  ...pendingApprovals(runtime, session.id)
139
139
  };
140
140
  }
141
+ /** Agent state views omit execution identities; low-level CLI queries keep their own contract. */
142
+ export function agentNetworkSessionView(runtime, session, queuedCount) {
143
+ const { runId: _runId, latestMessage, recentTools, pendingApprovals: approvals, ...state } = marctlSessionDetail(runtime, session, queuedCount);
144
+ return {
145
+ ...state,
146
+ latestMessage: latestMessage === null
147
+ ? null
148
+ : {
149
+ text: latestMessage.text,
150
+ state: latestMessage.state,
151
+ at: latestMessage.at,
152
+ truncated: latestMessage.truncated
153
+ },
154
+ recentTools: recentTools.map(({ runId: _runId, ...tool }) => tool),
155
+ pendingApprovals: approvals.map(({ runId: _runId, ...approval }) => approval)
156
+ };
157
+ }
141
158
  function time(item) {
142
159
  return item.updatedAt ?? item.completedAt ?? item.startedAt;
143
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.11.51",
3
+ "version": "0.11.52",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -38,10 +38,10 @@
38
38
  "sharp": "0.35.5",
39
39
  "ws": "^8.21.3",
40
40
  "zod": "4.4.3",
41
- "@myagentroam/agent": "0.11.51",
41
+ "@myagentroam/agent": "0.11.52",
42
42
  "@myagentroam/code-navigation": "0.11.1",
43
- "@myagentroam/protocol": "0.11.51",
44
- "@myagentroam/marctl": "0.11.10"
43
+ "@myagentroam/marctl": "0.11.11",
44
+ "@myagentroam/protocol": "0.11.52"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.18.1"