@myagentroam/node 0.11.51 → 0.11.53
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 +46 -8
- package/dist/marctl-guidance.js +6 -3
- package/dist/runner/mar-agent/agent-network-extension.js +6 -9
- package/dist/runner/mar-agent/managed-run-controller.js +0 -38
- package/dist/runner/mar-agent/runtime-model-factory.js +6 -42
- package/dist/service/agent-network-bindings.js +31 -27
- package/dist/service/agent-network-invoke.js +3 -2
- package/dist/service/agent-network-list.js +17 -6
- package/dist/service/agent-network-service.js +32 -19
- package/dist/service/agent-network-wait.js +4 -3
- package/dist/service/goal-continuation-coordinator.js +3 -0
- package/dist/service/planned-restart-handoff.js +2 -4
- package/dist/service/restart-execution-state.js +0 -15
- package/dist/service/restart-session-restorer.js +2 -4
- package/dist/service/run-authorization-control.js +21 -13
- package/dist/service/run-ownership-policy.js +3 -2
- package/dist/service/run-workbench-service.js +19 -10
- package/dist/service/session-recent-activity.js +17 -0
- package/package.json +4 -4
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:
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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) => {
|
package/dist/marctl-guidance.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
81
|
-
properties: { agentId: text
|
|
82
|
-
required: ['agentId'
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
183
|
-
?
|
|
184
|
-
:
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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:
|
|
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
|
|
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
|
|
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()]
|
|
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,
|
|
61
|
-
return {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
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
|
|
201
|
+
const result = this.options.stop(actor, session);
|
|
198
202
|
return {
|
|
199
203
|
agentId: session.id,
|
|
200
|
-
|
|
201
|
-
|
|
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 === '
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 ||
|
|
30
|
+
if (pending === undefined ||
|
|
31
|
+
pending.generation !== this.#generation ||
|
|
32
|
+
pending.responseType !== envelope.type)
|
|
33
33
|
return true;
|
|
34
|
-
const parsed =
|
|
34
|
+
const parsed = executionPrepareResponseSchema.safeParse(envelope.payload);
|
|
35
35
|
if (!parsed.success)
|
|
36
|
-
pending.reject(new Error(
|
|
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(
|
|
44
|
-
const envelope = createEnvelope(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
26
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
3
|
+
"version": "0.11.53",
|
|
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",
|
|
42
41
|
"@myagentroam/code-navigation": "0.11.1",
|
|
43
|
-
"@myagentroam/
|
|
44
|
-
"@myagentroam/
|
|
42
|
+
"@myagentroam/marctl": "0.11.11",
|
|
43
|
+
"@myagentroam/agent": "0.11.53",
|
|
44
|
+
"@myagentroam/protocol": "0.11.53"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.18.1"
|