@myagentroam/node 0.10.57 → 0.10.59
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 +3 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +61 -9
- package/dist/runner/mar-agent/marctl-skill.js +4 -4
- package/dist/runtime-state.js +23 -3
- package/dist/service/node-run-event-bridge.js +10 -0
- package/dist/service/session-catalog-service.js +9 -1
- package/dist/service/session-query-service.js +9 -3
- package/dist/service/session-recent-activity.js +177 -0
- package/dist/service/workspace-queue-workbench-service.js +5 -0
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -56,6 +56,7 @@ import { SessionContextService } from './service/session-context-service.js';
|
|
|
56
56
|
import { WorkspaceDomainState } from './service/workspace-domain-state.js';
|
|
57
57
|
import { RunWorkbenchService } from './service/run-workbench-service.js';
|
|
58
58
|
import { SessionQueryService } from './service/session-query-service.js';
|
|
59
|
+
import { marctlSessionProgress } from './service/session-recent-activity.js';
|
|
59
60
|
import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbench-service.js';
|
|
60
61
|
import { ContinuousRunCoordinator } from './service/continuous-run-coordinator.js';
|
|
61
62
|
import { SessionIdentityService } from './service/session-identity-service.js';
|
|
@@ -500,6 +501,7 @@ export class NodeConnector {
|
|
|
500
501
|
sequence: this.workbenchEventService.snapshotSequence(),
|
|
501
502
|
session: this.sessionPresentationService.present(session),
|
|
502
503
|
runs: this.runtime.listRunsForSession(session.id, { includeTerminal: true }),
|
|
504
|
+
progress: marctlSessionProgress(this.runtime, session.id),
|
|
503
505
|
performance: this.runtime.sessionPerformance(session.id),
|
|
504
506
|
queue: this.workspaceQueueWorkbenchService.snapshot(session.id),
|
|
505
507
|
context,
|
|
@@ -787,6 +789,7 @@ export class NodeConnector {
|
|
|
787
789
|
markInterrupted: (runId) => this.runState.interruptRequested.add(runId)
|
|
788
790
|
});
|
|
789
791
|
this.sessionQueryService = new SessionQueryService({
|
|
792
|
+
queuedCount: (sessionId) => this.workspaceQueueWorkbenchService.queuedCount(sessionId),
|
|
790
793
|
workspace: (workspaceId) => this.database?.getWorkspace(workspaceId),
|
|
791
794
|
goal: (sessionId) => this.goalRuntime.get(sessionId),
|
|
792
795
|
runtime: this.runtime,
|
|
@@ -6,6 +6,7 @@ export function normalizeMarAgentUsage(usage) {
|
|
|
6
6
|
}
|
|
7
7
|
import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
|
|
8
8
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
9
|
+
import { toolPresentationSchema } from '@myagentroam/protocol';
|
|
9
10
|
export class MarAgentConversationNormalizer {
|
|
10
11
|
input(entry) {
|
|
11
12
|
return {
|
|
@@ -89,8 +90,11 @@ export class MarAgentConversationNormalizer {
|
|
|
89
90
|
if (event.type === 'tool.started' ||
|
|
90
91
|
event.type === 'tool.completed' ||
|
|
91
92
|
event.type === 'tool.failed') {
|
|
92
|
-
//
|
|
93
|
-
if (event.parentCallId !== undefined &&
|
|
93
|
+
// Native web calls and generated artifacts stay visible independently of cell observations.
|
|
94
|
+
if (event.parentCallId !== undefined &&
|
|
95
|
+
event.toolName !== 'web_search' &&
|
|
96
|
+
event.toolName !== 'web_fetch' &&
|
|
97
|
+
!isImageGenerationTool(event.toolName))
|
|
94
98
|
return [];
|
|
95
99
|
if (event.type === 'tool.started')
|
|
96
100
|
return this.#toolStarted(event);
|
|
@@ -101,6 +105,15 @@ export class MarAgentConversationNormalizer {
|
|
|
101
105
|
...this.#codeModeToolResults(event)
|
|
102
106
|
];
|
|
103
107
|
}
|
|
108
|
+
if (event.type === 'sleep.started') {
|
|
109
|
+
const state = this.#tools.get(event.callId);
|
|
110
|
+
if (state?.toolName !== 'sleep')
|
|
111
|
+
return [];
|
|
112
|
+
this.#tools.set(event.callId, { ...state, sleepStartedAt: event.startedAt });
|
|
113
|
+
return [
|
|
114
|
+
genericToolItem(state.itemId, 'sleep', 'IN_PROGRESS', state.input, null, null, false, undefined, undefined, undefined, event.startedAt)
|
|
115
|
+
];
|
|
116
|
+
}
|
|
104
117
|
if (event.type === 'todo.updated')
|
|
105
118
|
return [
|
|
106
119
|
{
|
|
@@ -205,13 +218,14 @@ export class MarAgentConversationNormalizer {
|
|
|
205
218
|
input: event.input,
|
|
206
219
|
itemId: ownItemId,
|
|
207
220
|
structured: 'input' in event,
|
|
221
|
+
...(event.data === undefined ? {} : { presentationData: event.data }),
|
|
208
222
|
...(subagentTitle === undefined ? {} : { subagentTitle }),
|
|
209
223
|
...(subagentDetails === undefined ? {} : { subagentDetails })
|
|
210
224
|
});
|
|
211
225
|
if (event.toolName === 'apply_patch' && 'input' in event)
|
|
212
226
|
return [fileChangeItem(ownItemId, 'IN_PROGRESS', [])];
|
|
213
227
|
return [
|
|
214
|
-
genericToolItem(ownItemId, event.toolName, 'IN_PROGRESS', event.input, null, null, event.truncated === true,
|
|
228
|
+
genericToolItem(ownItemId, event.toolName, 'IN_PROGRESS', event.input, null, null, event.truncated === true, event.data, subagentTitle, subagentDetails)
|
|
215
229
|
];
|
|
216
230
|
}
|
|
217
231
|
#compactAnchorItemId(event, publicItemIdPrefix) {
|
|
@@ -278,7 +292,7 @@ export class MarAgentConversationNormalizer {
|
|
|
278
292
|
}
|
|
279
293
|
const item = genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, 'COMPLETED', state?.input, generatedImages.length === 0
|
|
280
294
|
? event.summary
|
|
281
|
-
: generatedImages.map((image) => image.name).join(', '), null, event.truncated, event.data, agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details);
|
|
295
|
+
: generatedImages.map((image) => image.name).join(', '), null, event.truncated, event.data, agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details, state?.sleepStartedAt);
|
|
282
296
|
return [
|
|
283
297
|
{
|
|
284
298
|
...item,
|
|
@@ -306,7 +320,9 @@ export class MarAgentConversationNormalizer {
|
|
|
306
320
|
fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'FAILED', patchChanges(event.data, event.resultExtensions))
|
|
307
321
|
];
|
|
308
322
|
return [
|
|
309
|
-
genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName,
|
|
323
|
+
genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, event.code === 'MAR_AGENT_CANCELLED' || event.code === 'MAR_AGENT_EXECUTION_CANCELLED'
|
|
324
|
+
? 'CANCELLED'
|
|
325
|
+
: 'FAILED', state?.input, event.message ?? event.code, event.code, event.truncated === true, state?.presentationData, state?.subagentTitle, state?.subagentDetails, state?.sleepStartedAt)
|
|
310
326
|
];
|
|
311
327
|
}
|
|
312
328
|
#codeModeToolResults(event) {
|
|
@@ -580,7 +596,7 @@ function normalizeContextCompaction(event, anchorItemId) {
|
|
|
580
596
|
function validCompactAnchor(value) {
|
|
581
597
|
return value !== undefined && value.length > 0 && value.length <= 128 ? value : undefined;
|
|
582
598
|
}
|
|
583
|
-
function genericToolItem(id, toolName, status, input, outputSummary = null, errorCode = null, truncated = false, output, subagentTitle, knownSubagentDetails) {
|
|
599
|
+
function genericToolItem(id, toolName, status, input, outputSummary = null, errorCode = null, truncated = false, output, subagentTitle, knownSubagentDetails, sleepStartedAt) {
|
|
584
600
|
if (toolName === 'code_exec') {
|
|
585
601
|
const code = typeof input === 'string' ? input : record(input)?.code;
|
|
586
602
|
const source = typeof code === 'string' ? code : compactRunnerValue(input, 10_000).text || toolName;
|
|
@@ -594,7 +610,7 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
594
610
|
outputPreview: compactRunnerText(output, 10_000),
|
|
595
611
|
exitCode: null,
|
|
596
612
|
truncated: truncated || source.length > 10_000 || output.length > 10_000
|
|
597
|
-
}, status);
|
|
613
|
+
}, status, 'Code execution');
|
|
598
614
|
}
|
|
599
615
|
if (toolName === 'code_exec_wait') {
|
|
600
616
|
const inputRecord = record(input);
|
|
@@ -622,10 +638,11 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
622
638
|
inputSummary.truncated ||
|
|
623
639
|
result?.truncated === true ||
|
|
624
640
|
resultSummary.length > 10_000
|
|
625
|
-
}, status);
|
|
641
|
+
}, status, 'Code execution wait');
|
|
626
642
|
}
|
|
627
643
|
const subagent = marAgentSubagentProjection(toolName, input, output, subagentTitle, knownSubagentDetails);
|
|
628
644
|
const toolCategory = isImageGenerationTool(toolName) ? 'IMAGE_GENERATION' : undefined;
|
|
645
|
+
const presentation = projectToolPresentation(toolName, input, output, sleepStartedAt);
|
|
629
646
|
const inputSummary = compactRunnerValue(input, 10_000);
|
|
630
647
|
return {
|
|
631
648
|
itemId: id,
|
|
@@ -641,17 +658,52 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
641
658
|
errorCode: errorCode === null ? null : compactRunnerText(errorCode, 80),
|
|
642
659
|
truncated: truncated || inputSummary.truncated || (outputSummary?.length ?? 0) > 10_000,
|
|
643
660
|
...(toolCategory === undefined ? {} : { toolCategory }),
|
|
661
|
+
...(presentation === undefined ? {} : { toolPresentation: presentation }),
|
|
644
662
|
...subagent
|
|
645
663
|
},
|
|
646
664
|
merge: true
|
|
647
665
|
};
|
|
648
666
|
}
|
|
649
|
-
function
|
|
667
|
+
function projectToolPresentation(toolName, input, output, startedAt) {
|
|
668
|
+
const category = toolName === 'web_search'
|
|
669
|
+
? 'WEB_SEARCH'
|
|
670
|
+
: toolName === 'web_fetch'
|
|
671
|
+
? 'WEB_FETCH'
|
|
672
|
+
: toolName === 'skill'
|
|
673
|
+
? 'SKILL'
|
|
674
|
+
: toolName === 'sleep'
|
|
675
|
+
? 'SLEEP'
|
|
676
|
+
: undefined;
|
|
677
|
+
if (category === undefined)
|
|
678
|
+
return undefined;
|
|
679
|
+
const args = record(input) ?? {};
|
|
680
|
+
const data = record(output) ?? {};
|
|
681
|
+
// A provider-hosted search tool can report a page-read action under its native name.
|
|
682
|
+
const kind = category === 'WEB_SEARCH' && data.hostedToolKind === 'WEB_FETCH' ? 'WEB_FETCH' : category;
|
|
683
|
+
const text = (value, limit) => typeof value === 'string' ? compactRunnerText(value, limit) : undefined;
|
|
684
|
+
const candidate = kind === 'WEB_SEARCH'
|
|
685
|
+
? { ...data, kind, query: text(data.query ?? args.query, 500) }
|
|
686
|
+
: kind === 'WEB_FETCH'
|
|
687
|
+
? {
|
|
688
|
+
...data,
|
|
689
|
+
kind,
|
|
690
|
+
requestUrl: text(args.url, 8192),
|
|
691
|
+
url: text(data.url, 8192),
|
|
692
|
+
mode: data.mode ?? args.mode
|
|
693
|
+
}
|
|
694
|
+
: kind === 'SKILL'
|
|
695
|
+
? { ...data, kind, name: text(data.name ?? args.name, 128) }
|
|
696
|
+
: { ...data, kind, durationMs: data.durationMs ?? args.durationMs, startedAt };
|
|
697
|
+
const parsed = toolPresentationSchema.safeParse(candidate);
|
|
698
|
+
return parsed.success ? parsed.data : { kind };
|
|
699
|
+
}
|
|
700
|
+
function commandItem(command, status, title) {
|
|
650
701
|
return {
|
|
651
702
|
itemId: command.itemId,
|
|
652
703
|
kind: 'command_execution',
|
|
653
704
|
status,
|
|
654
705
|
payload: {
|
|
706
|
+
...(title === undefined ? {} : { title }),
|
|
655
707
|
command: command.command,
|
|
656
708
|
cwd: command.cwd,
|
|
657
709
|
outputPreview: command.outputPreview,
|
|
@@ -25,14 +25,14 @@ const marctlInstructions = [
|
|
|
25
25
|
'Prefer -m / --message for short text; use --message-file for longer UTF-8 text or an existing file (- reads stdin). These options are mutually exclusive. Example: marctl sessions start --runner codex --title "fixer#42 Handle empty totals" -m "Implement issue #42 and report test evidence" --json. Other Runner names are mar-agent, claude-code and opencode. An omitted model uses the target Runner configuration, not the Coordinator model.',
|
|
26
26
|
'For start/send, repeat --image <local-path> to attach PNG, JPEG, GIF or WebP images, with or without text. Images are read on this CLI host, not by remote path. Limits: 5 images, 5 MiB each, 10 MiB total. Actual Runner/model image capabilities apply; goal set accepts text only.',
|
|
27
27
|
'Create workers only as tasks need them, named fixer#<task-id> <description> or reviewer#<task-or-change-id> <description>. These are conventions, not product roles or a fixed pool. Put role, scope, acceptance criteria and delivery requirements in the first message. Follow the repository workflow for development and review. Session Prompt is edited in the Web UI only; marctl has no editing command, and sends to existing Sessions use normal prompt injection.',
|
|
28
|
-
'Reuse recorded Session/Run IDs and receipts. Refresh
|
|
29
|
-
'show --json fields: session.
|
|
28
|
+
'Reuse recorded Session/Run IDs and receipts. Refresh execution state with show only when the next action depends on it; use goal get or schedule get for those details. Select needed JSON fields instead of dumping full Session lists or tool transcripts. Hidden reasoning, credentials and raw model requests are not collaboration artifacts.',
|
|
29
|
+
'list returns id/title/runner/status/runId and one latestActivity per Session. show --json fields: session.id, session.title, session.runner, session.status, session.runId, session.model, session.effort, session.queuedCount, session.latestMessage, session.recentTools (at most three). Status is RUNNING/IDLE/SLEEPING; tools keep their original states. Activity is bounded public evidence, not proof of task completion; use history for full results.',
|
|
30
30
|
'Rename any authorized Workspace Session with sessions rename <session-id> --title <name> --json; parent relationships do not restrict renaming.',
|
|
31
31
|
'show/config/history/graph/rename and schedule get/create/update may omit the Session ID to use the current Session. Without a current Session, specify an ID. send/goal/stop/wait always require explicit target IDs.',
|
|
32
32
|
'Use sessions schedule list/get/create/update for the same tasks as the built-in schedule MCP, which is hidden while this skill is available. list is Workspace-scoped. create requires --cron and text (-m/--message-file, up to 20,000 characters). update requires --expected-revision from get and at least cron or text. get returns task (null if absent), with id, revision, objective, cron, timezone and enabled; applied updates also return task. Reuse that task instead of probing its shape or refetching unchanged state. Schedule text is trimmed at both ends; compare after the same trimming; a conclusive update response needs no extra get. Other-owner edits return a PENDING proposal, not an applied change. Scheduled execution uses normal user permissions. Follow existing approval rules and do not retry conflicts or uncertain writes blindly.',
|
|
33
33
|
'Read results with sessions history <session-id> --limit 10 --json (default: latest 10 SEGMENTs, not messages; --limit 1..100). In segments[].items[], select kind === "assistant_message" and read payload.text; keep segment runId and user_message.payload.clientMessageId for queued-input correlation. Reuse the returned page; follow --cursor <nextCursor> only for missing evidence.',
|
|
34
|
-
'Use sessions send <session-id> --steer -m "Additional context: focus on the resume path." --json for decision-changing updates rather than routine reports: it may switch the current execution. Node decides delivery from actual state; an idle Session receives ordinary input. Choose exactly one of --steer or --queue, even when idle; missing, conflicting, repeated, or obsolete options are rejected locally before input is read or a request is sent. Use --queue to place input in a subsequent turn; with an active Goal, ordinary queued input waits until the Goal exits. start/send return clientMessageId and the accepted/queued result. A queued result may refer to the currently active Run: associate queueItemId/clientMessageId with history before deciding that input finished. There are no queue-item edit/delete/reorder commands; show
|
|
35
|
-
'Wait with sessions wait <session-id> --run <run-id> --timeout 5m --json through the existing Watch. Prefer --timeout 5m when blocked on task completion; use 30s for a short deadline. Continue useful independent work before waiting. If exec returns a running process or cell, reuse its process/cell handle rather than starting another wait or polling show/list. Timeout or cancelled waiting does not stop the task. Use sessions stop <session-id> --run <run-id> --json only to stop that active Run; ownership rules still apply.',
|
|
34
|
+
'Use sessions send <session-id> --steer -m "Additional context: focus on the resume path." --json for decision-changing updates rather than routine reports: it may switch the current execution. Node decides delivery from actual state; an idle Session receives ordinary input. Choose exactly one of --steer or --queue, even when idle; missing, conflicting, repeated, or obsolete options are rejected locally before input is read or a request is sent. Use --queue to place input in a subsequent turn; with an active Goal, ordinary queued input waits until the Goal exits. start/send return clientMessageId and the accepted/queued result. A queued result may refer to the currently active Run: associate queueItemId/clientMessageId with history before deciding that input finished. There are no queue-item edit/delete/reorder commands; show returns only queuedCount.',
|
|
35
|
+
'Wait with sessions wait <session-id> --run <run-id> --timeout 5m --json through the existing Watch. Prefer --timeout 5m when blocked on task completion; use 30s for a short deadline. Continue useful independent work before waiting. If exec returns a running process or cell, reuse its process/cell handle rather than starting another wait or polling show/list. A timeout may include progress, the last observed snapshot, not a fresh query. Timeout or cancelled waiting does not stop the task. Use sessions stop <session-id> --run <run-id> --json only to stop that active Run; ownership rules still apply.',
|
|
36
36
|
'Goal get/set/clear use the existing Session Goal semantics; see marctl help sessions. A Coordinator can work from ordinary messages or its existing schedule without a Goal. Worker completion never automatically wakes it. Actively wait/read results, or finish and reconcile on the next user message or scheduled input.',
|
|
37
37
|
'## Verify, recover and respect authorization',
|
|
38
38
|
'An accepted message or SUCCEEDED Run is not proof of business completion. Inspect artifacts and test evidence.',
|
package/dist/runtime-state.js
CHANGED
|
@@ -702,7 +702,12 @@ export class NodeRuntimeState {
|
|
|
702
702
|
completedAt: terminal(next) ? this.now() : turn.completedAt,
|
|
703
703
|
items: terminal(next)
|
|
704
704
|
? turn.items.map((item) => activeConversationItem(item.status)
|
|
705
|
-
? {
|
|
705
|
+
? {
|
|
706
|
+
...item,
|
|
707
|
+
status: completedStatusForRun(next),
|
|
708
|
+
completedAt: this.now(),
|
|
709
|
+
updatedAt: now
|
|
710
|
+
}
|
|
706
711
|
: item)
|
|
707
712
|
: turn.items
|
|
708
713
|
}));
|
|
@@ -844,6 +849,9 @@ export class NodeRuntimeState {
|
|
|
844
849
|
const turns = this.listConversationTurns(input).turns;
|
|
845
850
|
return { items: turns.flatMap((turn) => turn.items), nextCursor: null };
|
|
846
851
|
}
|
|
852
|
+
conversationTail(sessionId) {
|
|
853
|
+
return this.turns.get(this.resolvePromotedSessionId(sessionId)) ?? [];
|
|
854
|
+
}
|
|
847
855
|
recordCommandExecution(input) {
|
|
848
856
|
const session = this.requireSession(input.sessionId);
|
|
849
857
|
const now = this.now();
|
|
@@ -1310,7 +1318,12 @@ export class NodeRuntimeState {
|
|
|
1310
1318
|
status,
|
|
1311
1319
|
completedAt,
|
|
1312
1320
|
items: turn.items.map((item) => activeConversationItem(item.status)
|
|
1313
|
-
? {
|
|
1321
|
+
? {
|
|
1322
|
+
...item,
|
|
1323
|
+
status: completedStatusForRun(status),
|
|
1324
|
+
completedAt,
|
|
1325
|
+
updatedAt: completedAt
|
|
1326
|
+
}
|
|
1314
1327
|
: item)
|
|
1315
1328
|
}
|
|
1316
1329
|
: turn));
|
|
@@ -1347,6 +1360,7 @@ export class NodeRuntimeState {
|
|
|
1347
1360
|
sourceSequence: existing?.sourceSequence ?? event.sequence,
|
|
1348
1361
|
revision: event.sequence,
|
|
1349
1362
|
kind: 'assistant_message',
|
|
1363
|
+
updatedAt: event.createdAt,
|
|
1350
1364
|
status: 'IN_PROGRESS',
|
|
1351
1365
|
payload: { text, format: 'markdown', model: null },
|
|
1352
1366
|
startedAt: existing?.startedAt ?? event.createdAt,
|
|
@@ -1369,7 +1383,12 @@ export class NodeRuntimeState {
|
|
|
1369
1383
|
trailingItem?.kind === 'assistant_message' &&
|
|
1370
1384
|
activeConversationItem(trailingItem.status)
|
|
1371
1385
|
? turn.items.map((item) => item.id === trailingItem.id
|
|
1372
|
-
? {
|
|
1386
|
+
? {
|
|
1387
|
+
...item,
|
|
1388
|
+
status: 'COMPLETED',
|
|
1389
|
+
completedAt: event.createdAt,
|
|
1390
|
+
updatedAt: event.createdAt
|
|
1391
|
+
}
|
|
1373
1392
|
: item)
|
|
1374
1393
|
: turn.items;
|
|
1375
1394
|
const existing = orderedItems.find((item) => item.id === itemId) ??
|
|
@@ -1390,6 +1409,7 @@ export class NodeRuntimeState {
|
|
|
1390
1409
|
sourceSequence: existing?.sourceSequence ?? event.sequence,
|
|
1391
1410
|
revision: (existing?.revision ?? -1) + 1,
|
|
1392
1411
|
kind: projected.kind,
|
|
1412
|
+
updatedAt: event.createdAt,
|
|
1393
1413
|
status,
|
|
1394
1414
|
payload: mergeConversationItemPayload(existing?.payload, projected),
|
|
1395
1415
|
...(projected.permissionStop === undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
|
|
2
2
|
import { runConversationItemId } from '../runtime-state.js';
|
|
3
|
+
import { marctlSessionProgress } from './session-recent-activity.js';
|
|
3
4
|
import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
4
5
|
export function workbenchPayloadSessionId(runtime, category, payload) {
|
|
5
6
|
if (!isPlainRecord(payload))
|
|
@@ -82,6 +83,10 @@ export class NodeRunEventBridge {
|
|
|
82
83
|
payload = {
|
|
83
84
|
sessionId: turn.sessionId,
|
|
84
85
|
runId: turn.runId,
|
|
86
|
+
...(['assistant_message', 'tool_call', 'command_execution', 'file_change'].includes(item.kind) &&
|
|
87
|
+
(rawPayload?.append === undefined || item.status !== 'IN_PROGRESS')
|
|
88
|
+
? { progress: marctlSessionProgress(this.options.runtime, turn.sessionId) }
|
|
89
|
+
: {}),
|
|
85
90
|
item: {
|
|
86
91
|
turnId: turn.id,
|
|
87
92
|
itemId: item.id,
|
|
@@ -105,6 +110,11 @@ export class NodeRunEventBridge {
|
|
|
105
110
|
isPlainRecord(payload) &&
|
|
106
111
|
isNodeConversationTurn(payload.turn))
|
|
107
112
|
return { ...payload, segments: conversationSegments(payload.turn) };
|
|
113
|
+
if (category === 'run' && isPlainRecord(payload) && isNodeAgentRun(payload.run))
|
|
114
|
+
return {
|
|
115
|
+
...payload,
|
|
116
|
+
progress: marctlSessionProgress(this.options.runtime, payload.run.sessionId)
|
|
117
|
+
};
|
|
108
118
|
return payload;
|
|
109
119
|
}
|
|
110
120
|
emitConversationItem(runId, item) {
|
|
@@ -2,6 +2,7 @@ import { removeNativeSession } from '../native-session-history.js';
|
|
|
2
2
|
import { nodeLog } from '../operational.js';
|
|
3
3
|
import { isTerminalRunStatus, sessionPage } from '../util/node-operation-parsers.js';
|
|
4
4
|
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
5
|
+
import { marctlSessionSummary } from './session-recent-activity.js';
|
|
5
6
|
export class SessionCatalogService {
|
|
6
7
|
options;
|
|
7
8
|
rewindQueue = Promise.resolve();
|
|
@@ -20,7 +21,14 @@ export class SessionCatalogService {
|
|
|
20
21
|
}
|
|
21
22
|
async list(input) {
|
|
22
23
|
const sessions = await this.collect(input);
|
|
23
|
-
|
|
24
|
+
const page = sessionPage(sessions.filter((session) => input.parentSessionId === undefined || session.parentSessionId === input.parentSessionId), input);
|
|
25
|
+
return input.view === 'marctl'
|
|
26
|
+
? {
|
|
27
|
+
view: 'marctl',
|
|
28
|
+
sessions: page.sessions.map((session) => marctlSessionSummary(this.options.runtime, session)),
|
|
29
|
+
nextCursor: page.nextCursor
|
|
30
|
+
}
|
|
31
|
+
: page;
|
|
24
32
|
}
|
|
25
33
|
async graph(input) {
|
|
26
34
|
if (typeof input.workspaceId !== 'string')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { marctlSessionDetail } from './session-recent-activity.js';
|
|
1
2
|
import { isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
2
3
|
import { toolResultResourceInputSchema, toolResultResourcePageSchema } from '@myagentroam/protocol';
|
|
3
4
|
export class SessionQueryService {
|
|
@@ -23,9 +24,14 @@ export class SessionQueryService {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
async get(input) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
|
|
28
|
+
return input.view === 'marctl'
|
|
29
|
+
? {
|
|
30
|
+
view: 'marctl',
|
|
31
|
+
workspaceId: session.workspaceId,
|
|
32
|
+
session: marctlSessionDetail(this.options.runtime, session, this.options.queuedCount(session.id))
|
|
33
|
+
}
|
|
34
|
+
: { session: this.options.present(session) };
|
|
29
35
|
}
|
|
30
36
|
async cliContext(input) {
|
|
31
37
|
if (typeof input.workspaceId !== 'string')
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
|
|
2
|
+
/** Projects only already-observed, process-local public facts. Never reads native history. */
|
|
3
|
+
function activity(runtime, sessionId) {
|
|
4
|
+
const candidate = runtime.activeSessionRun(sessionId);
|
|
5
|
+
const run = candidate && !isTerminalRunStatus(candidate.status) ? candidate : undefined;
|
|
6
|
+
const turns = runtime.conversationTail(sessionId);
|
|
7
|
+
let sleeping = false;
|
|
8
|
+
let latestActivity = null;
|
|
9
|
+
let latestMessage = null;
|
|
10
|
+
const recentTools = [];
|
|
11
|
+
for (let t = turns.length - 1; t >= 0; t -= 1) {
|
|
12
|
+
const turn = turns[t];
|
|
13
|
+
if (run && turn.runId !== run.id)
|
|
14
|
+
continue;
|
|
15
|
+
for (let i = turn.items.length - 1; i >= 0; i -= 1) {
|
|
16
|
+
const item = turn.items[i];
|
|
17
|
+
if (item.parentItemId !== null)
|
|
18
|
+
continue;
|
|
19
|
+
const payload = isPlainRecord(item.payload) ? item.payload : {};
|
|
20
|
+
const at = time(item);
|
|
21
|
+
const isLatest = isNewerActivity(latestActivity, at, item.kind === 'assistant_message' ? 'message' : 'tool');
|
|
22
|
+
if (item.kind === 'assistant_message' &&
|
|
23
|
+
(latestMessage === null || isLatest) &&
|
|
24
|
+
typeof payload.text === 'string' &&
|
|
25
|
+
payload.text.trim()) {
|
|
26
|
+
if (latestMessage === null) {
|
|
27
|
+
const text = clip(payload.text, 300);
|
|
28
|
+
latestMessage = {
|
|
29
|
+
text: text.text,
|
|
30
|
+
state: item.status,
|
|
31
|
+
at,
|
|
32
|
+
runId: item.runId,
|
|
33
|
+
truncated: text.truncated
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (isLatest) {
|
|
37
|
+
const summary = clip(payload.text, 100);
|
|
38
|
+
latestActivity = {
|
|
39
|
+
state: item.status,
|
|
40
|
+
at,
|
|
41
|
+
runId: item.runId,
|
|
42
|
+
kind: 'message',
|
|
43
|
+
name: 'assistant',
|
|
44
|
+
summary: summary.text,
|
|
45
|
+
truncated: summary.truncated
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!['tool_call', 'command_execution', 'file_change'].includes(item.kind))
|
|
50
|
+
continue;
|
|
51
|
+
const presentation = isPlainRecord(payload.toolPresentation) ? payload.toolPresentation : {};
|
|
52
|
+
if (run?.status === 'RUNNING' &&
|
|
53
|
+
item.status === 'IN_PROGRESS' &&
|
|
54
|
+
presentation.kind === 'SLEEP' &&
|
|
55
|
+
typeof presentation.startedAt === 'string')
|
|
56
|
+
sleeping = true;
|
|
57
|
+
if (recentTools.length === 3 && !isLatest)
|
|
58
|
+
continue;
|
|
59
|
+
const tool = projectTool(item, payload, presentation);
|
|
60
|
+
if (recentTools.length < 3)
|
|
61
|
+
recentTools.push(tool);
|
|
62
|
+
if (isLatest)
|
|
63
|
+
latestActivity = { ...tool, kind: 'tool' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
status: run === undefined
|
|
68
|
+
? 'IDLE'
|
|
69
|
+
: sleeping
|
|
70
|
+
? 'SLEEPING'
|
|
71
|
+
: 'RUNNING',
|
|
72
|
+
runId: run?.id ?? null,
|
|
73
|
+
latestActivity,
|
|
74
|
+
latestMessage,
|
|
75
|
+
recentTools,
|
|
76
|
+
observedAt: Math.max(latestActivity?.at ?? 0, run?.updatedAt ?? 0) || null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function marctlSessionProgress(runtime, sessionId) {
|
|
80
|
+
const { latestActivity: _latest, ...progress } = activity(runtime, sessionId);
|
|
81
|
+
return progress;
|
|
82
|
+
}
|
|
83
|
+
function identity(session) {
|
|
84
|
+
return {
|
|
85
|
+
id: session.id,
|
|
86
|
+
title: session.customTitle ?? session.runnerTitle ?? 'Untitled session',
|
|
87
|
+
runner: session.runner
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function marctlSessionSummary(runtime, session) {
|
|
91
|
+
const { status, runId, latestActivity } = activity(runtime, session.id);
|
|
92
|
+
if (latestActivity === null)
|
|
93
|
+
return { ...identity(session), status, runId, latestActivity: null };
|
|
94
|
+
const { runId: _activityRun, ...value } = latestActivity;
|
|
95
|
+
return { ...identity(session), status, runId, latestActivity: value };
|
|
96
|
+
}
|
|
97
|
+
export function marctlSessionDetail(runtime, session, queuedCount) {
|
|
98
|
+
const { status, runId, latestMessage, recentTools } = activity(runtime, session.id);
|
|
99
|
+
return {
|
|
100
|
+
...identity(session),
|
|
101
|
+
status,
|
|
102
|
+
runId,
|
|
103
|
+
model: session.model,
|
|
104
|
+
effort: session.effort,
|
|
105
|
+
queuedCount,
|
|
106
|
+
latestMessage,
|
|
107
|
+
recentTools
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function time(item) {
|
|
111
|
+
return item.updatedAt ?? item.completedAt ?? item.startedAt;
|
|
112
|
+
}
|
|
113
|
+
function isNewerActivity(current, at, kind) {
|
|
114
|
+
return (current === null ||
|
|
115
|
+
(at ?? 0) > (current.at ?? 0) ||
|
|
116
|
+
(at === current.at && kind === 'tool' && current.kind !== 'tool'));
|
|
117
|
+
}
|
|
118
|
+
function projectTool(item, payload, presentation) {
|
|
119
|
+
const name = clip(typeof payload.toolName === 'string' ? payload.toolName : item.kind, 128);
|
|
120
|
+
let summary = '';
|
|
121
|
+
if (presentation.kind === 'SLEEP') {
|
|
122
|
+
summary =
|
|
123
|
+
typeof presentation.elapsedMs === 'number'
|
|
124
|
+
? 'Slept for ' +
|
|
125
|
+
presentation.elapsedMs +
|
|
126
|
+
' ms' +
|
|
127
|
+
(typeof presentation.wakeReason === 'string'
|
|
128
|
+
? '; wake reason: ' + presentation.wakeReason
|
|
129
|
+
: '')
|
|
130
|
+
: typeof presentation.durationMs === 'number'
|
|
131
|
+
? 'Sleep for up to ' + presentation.durationMs + ' ms'
|
|
132
|
+
: 'Sleep';
|
|
133
|
+
}
|
|
134
|
+
else if (item.kind === 'command_execution') {
|
|
135
|
+
summary =
|
|
136
|
+
typeof payload.title === 'string'
|
|
137
|
+
? payload.title
|
|
138
|
+
: typeof payload.command === 'string'
|
|
139
|
+
? payload.command
|
|
140
|
+
: '';
|
|
141
|
+
if (typeof payload.exitCode === 'number')
|
|
142
|
+
summary += '; exit code ' + payload.exitCode;
|
|
143
|
+
}
|
|
144
|
+
else if (item.kind === 'file_change' && Array.isArray(payload.changes)) {
|
|
145
|
+
summary = payload.changes
|
|
146
|
+
.filter(isPlainRecord)
|
|
147
|
+
.map((change) => (typeof change.path === 'string' ? change.path : ''))
|
|
148
|
+
.join(', ');
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
summary = [
|
|
152
|
+
item.status === 'IN_PROGRESS' ? payload.progressLabel : undefined,
|
|
153
|
+
payload.title ?? payload.toolName,
|
|
154
|
+
payload.errorCode
|
|
155
|
+
]
|
|
156
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
157
|
+
.join(' · ');
|
|
158
|
+
}
|
|
159
|
+
const bounded = clip(summary.replace(/\s+/gu, ' ').trim(), 100);
|
|
160
|
+
return {
|
|
161
|
+
name: name.text,
|
|
162
|
+
state: item.status,
|
|
163
|
+
summary: bounded.text,
|
|
164
|
+
at: time(item),
|
|
165
|
+
runId: item.runId,
|
|
166
|
+
truncated: name.truncated || bounded.truncated || payload.truncated === true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function clip(value, limit) {
|
|
170
|
+
const chars = [];
|
|
171
|
+
for (const char of value) {
|
|
172
|
+
if (chars.length === limit)
|
|
173
|
+
return { text: chars.join(''), truncated: true };
|
|
174
|
+
chars.push(char);
|
|
175
|
+
}
|
|
176
|
+
return { text: chars.join(''), truncated: false };
|
|
177
|
+
}
|
|
@@ -172,6 +172,11 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
172
172
|
const item = this.options.runtime.getQueueItem(itemId);
|
|
173
173
|
return item?.clientMessageId.startsWith('goal:') ? undefined : item;
|
|
174
174
|
}
|
|
175
|
+
queuedCount(sessionId) {
|
|
176
|
+
return this.options.runtime
|
|
177
|
+
.sessionQueue(sessionId)
|
|
178
|
+
.itemIds.filter((id) => this.publicQueueItem(id) !== undefined).length;
|
|
179
|
+
}
|
|
175
180
|
requireItem(itemId, workspaceId) {
|
|
176
181
|
const item = this.options.runtime.getQueueItem(itemId);
|
|
177
182
|
if (item === undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.59",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.10.
|
|
40
|
+
"@myagentroam/agent": "0.10.59",
|
|
41
|
+
"@myagentroam/marctl": "0.10.12",
|
|
41
42
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/protocol": "0.10.
|
|
43
|
-
"@myagentroam/marctl": "0.10.11"
|
|
43
|
+
"@myagentroam/protocol": "0.10.59"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|