@myagentroam/node 0.11.68 → 0.11.70
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/application/workspace-application.js +3 -0
- package/dist/capabilities.js +3 -0
- package/dist/connector.js +1 -0
- package/dist/runner/claude/tool-normalizer.js +23 -1
- package/dist/runner/codex/tool-normalizer.js +4 -1
- package/dist/runner/mar-agent/agent-network-extension.js +1 -1
- package/dist/runner/mar-agent/agent-operation-presentation.js +92 -4
- package/dist/runner/mar-agent/conversation-normalizer.js +110 -12
- package/dist/runner/normalization.js +15 -0
- package/dist/runner/opencode/tool-normalizer.js +19 -1
- package/dist/service/native-session-watch-service.js +20 -9
- package/dist/service/session-watch-slot-coordinator.js +21 -7
- package/dist/service/web-tool-service.js +10 -8
- package/dist/service/workbench-manifest-service.js +1 -0
- package/dist/service/workspace-watch-controller.js +24 -3
- package/dist/session/session-catalog-service.js +90 -19
- package/dist/workspace/workspace-watch-service.js +25 -5
- package/dist/workspace/workspace-watch-snapshot-service.js +51 -22
- package/package.json +4 -4
|
@@ -23,6 +23,9 @@ export function composeWorkspaceApplication(input) {
|
|
|
23
23
|
uploads: resources.uploads,
|
|
24
24
|
changes: input.changes,
|
|
25
25
|
listSessions: input.listSessions,
|
|
26
|
+
...(input.sessionNavigation === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { sessionNavigation: input.sessionNavigation }),
|
|
26
29
|
invalidate: resources.invalidateFiles,
|
|
27
30
|
nodeId: input.nodeId,
|
|
28
31
|
nodeGeneration: input.nodeGeneration,
|
package/dist/capabilities.js
CHANGED
|
@@ -61,6 +61,7 @@ export function unavailableCapabilities() {
|
|
|
61
61
|
promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
|
|
62
62
|
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
63
63
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
64
|
+
sessionAside: { apiVersion: 1 },
|
|
64
65
|
sessionGroups: { apiVersion: 1 },
|
|
65
66
|
terminalDirect: terminalDirectCapability()
|
|
66
67
|
};
|
|
@@ -98,6 +99,7 @@ export function detectCapabilities(commands = {}) {
|
|
|
98
99
|
promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
|
|
99
100
|
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
100
101
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
102
|
+
sessionAside: { apiVersion: 1 },
|
|
101
103
|
sessionGroups: { apiVersion: 1 },
|
|
102
104
|
terminalDirect: terminalDirectCapability()
|
|
103
105
|
};
|
|
@@ -139,6 +141,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
|
|
|
139
141
|
promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
|
|
140
142
|
chatgptLogin: { apiVersion: 1, loopback: true },
|
|
141
143
|
workspaceDirectTransfer: workspaceDirectTransferCapability(),
|
|
144
|
+
sessionAside: { apiVersion: 1 },
|
|
142
145
|
sessionGroups: { apiVersion: 1 },
|
|
143
146
|
terminalDirect: terminalDirectCapability()
|
|
144
147
|
};
|
package/dist/connector.js
CHANGED
|
@@ -1433,6 +1433,7 @@ export class NodeConnector {
|
|
|
1433
1433
|
limit: 30,
|
|
1434
1434
|
...(sessionGroup === undefined ? {} : { sessionGroup })
|
|
1435
1435
|
}),
|
|
1436
|
+
sessionNavigation: async (workspaceId, query) => this.sessionCatalogService.navigation({ workspaceId, ...query }),
|
|
1436
1437
|
invalidatePrompt: (id) => this.promptCompletion?.invalidateWorkspace(id),
|
|
1437
1438
|
nodeId: () => this.config?.nodeId ?? 'runtime-node',
|
|
1438
1439
|
nodeGeneration: () => this.nodeGeneration,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { claudeContentBlocks } from './message-parsers.js';
|
|
2
|
-
import { compactRunnerText, compactRunnerValue, publicOutputSnapshot, publicToolOutput } from '../normalization.js';
|
|
2
|
+
import { compactRunnerText, compactRunnerValue, publicOutputSnapshot, publicToolOutput, subagentCallDetail } from '../normalization.js';
|
|
3
3
|
import { shellExecution } from '../execution-presentation.js';
|
|
4
4
|
import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
|
|
5
5
|
export class ClaudeUsageNormalizer {
|
|
@@ -176,6 +176,23 @@ export function normalizeClaudeTool(toolName, input, output, failed, truncated,
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
: {}),
|
|
179
|
+
...(toolName === 'WebFetch' && typeof values?.url === 'string'
|
|
180
|
+
? {
|
|
181
|
+
toolPresentation: {
|
|
182
|
+
kind: 'WEB_FETCH',
|
|
183
|
+
detailOnly: true,
|
|
184
|
+
requestUrl: compactRunnerText(values.url, 8192)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
: toolName === 'WebSearch' && typeof values?.query === 'string'
|
|
188
|
+
? {
|
|
189
|
+
toolPresentation: {
|
|
190
|
+
kind: 'WEB_SEARCH',
|
|
191
|
+
detailOnly: true,
|
|
192
|
+
query: compactRunnerText(values.query, 500)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
179
196
|
...subagent
|
|
180
197
|
}
|
|
181
198
|
};
|
|
@@ -229,6 +246,11 @@ function claudeSubagentProjection(toolName, input, output, knownSubagents) {
|
|
|
229
246
|
knownSubagents.set(subagentId, title);
|
|
230
247
|
return {
|
|
231
248
|
subagentOperation: operation,
|
|
249
|
+
...(operation === 'START'
|
|
250
|
+
? subagentCallDetail(record(input)?.prompt)
|
|
251
|
+
: operation === 'MESSAGE'
|
|
252
|
+
? subagentCallDetail(record(input)?.message)
|
|
253
|
+
: {}),
|
|
232
254
|
subagentId: subagentId === '*' ? null : (subagentId ?? null),
|
|
233
255
|
...(operation === 'STATUS' ? { subagentWait: inputData?.block === true } : {}),
|
|
234
256
|
...(title.length === 0 ? {} : { title })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compactRunnerText, publicToolOutput } from '../normalization.js';
|
|
1
|
+
import { compactRunnerText, publicToolOutput, subagentCallDetail } from '../normalization.js';
|
|
2
2
|
import { shellExecution } from '../execution-presentation.js';
|
|
3
3
|
export function normalizeCodexApproval(method) {
|
|
4
4
|
return method === 'item/commandExecution/requestApproval'
|
|
@@ -114,6 +114,9 @@ function codexSubagentProjection(namespace, toolName, input, output) {
|
|
|
114
114
|
: '';
|
|
115
115
|
return {
|
|
116
116
|
subagentOperation: operation,
|
|
117
|
+
...(operation === 'START' || operation === 'MESSAGE'
|
|
118
|
+
? subagentCallDetail(record(input)?.message)
|
|
119
|
+
: {}),
|
|
117
120
|
...(toolName === 'wait_agent' ? { subagentWait: true } : {}),
|
|
118
121
|
subagentId: Array.isArray(inputData?.targets) && inputData.targets.length > 1
|
|
119
122
|
? null
|
|
@@ -226,7 +226,7 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
226
226
|
: tool.action === 'view'
|
|
227
227
|
? { session: rows[0] }
|
|
228
228
|
: (rows[0] ?? {});
|
|
229
|
-
await call.publishEventData(agentOperationEvent(tool.action,
|
|
229
|
+
await call.publishEventData(agentOperationEvent(tool.action, operationInput, result).eventData);
|
|
230
230
|
}
|
|
231
231
|
: undefined;
|
|
232
232
|
const result = await invoke(executionId, tool.action, operationInput, call, onResolved);
|
|
@@ -61,6 +61,15 @@ export function agentOperationEvent(action, input, result) {
|
|
|
61
61
|
const operation = agentOperationPresentation(`agent_${action}`, input);
|
|
62
62
|
const value = isPlainRecord(result) ? result : {};
|
|
63
63
|
const args = isPlainRecord(input) ? input : {};
|
|
64
|
+
const message = action === 'start' || action === 'send' ? text(args.content, 2000) : undefined;
|
|
65
|
+
const title = action === 'start' ? text(args.title, 500) : undefined;
|
|
66
|
+
if (message || title)
|
|
67
|
+
operation.detail = {
|
|
68
|
+
...(title ? { title } : {}),
|
|
69
|
+
...(message
|
|
70
|
+
? { message, ...(message !== args.content ? { messageTruncated: true } : {}) }
|
|
71
|
+
: {})
|
|
72
|
+
};
|
|
64
73
|
const rows = action === 'list' ? value.sessions : action === 'wait' ? value.agents : undefined;
|
|
65
74
|
const records = Array.isArray(rows)
|
|
66
75
|
? rows
|
|
@@ -82,7 +91,31 @@ export function agentOperationEvent(action, input, result) {
|
|
|
82
91
|
if (!agentId)
|
|
83
92
|
return [];
|
|
84
93
|
const title = text(entry.title ?? (action === 'start' ? args.title : undefined), 500);
|
|
85
|
-
|
|
94
|
+
const status = ['list', 'view', 'wait'].includes(action) ? text(entry.status) : undefined;
|
|
95
|
+
const latest = action === 'wait' ? latestWaitMessage(entry.updates) : undefined;
|
|
96
|
+
const viewMessage = action === 'view' && isPlainRecord(entry.latestMessage) ? entry.latestMessage : undefined;
|
|
97
|
+
const summary = action === 'wait' ? text(entry.summary, 800) : undefined;
|
|
98
|
+
const rawMessage = summary ?? latest?.value ?? text(viewMessage?.text, 800);
|
|
99
|
+
const messageTruncated = summary
|
|
100
|
+
? entry.summaryTruncated === true || summary !== entry.summary
|
|
101
|
+
: latest
|
|
102
|
+
? latest.truncated === true
|
|
103
|
+
: viewMessage?.truncated === true ||
|
|
104
|
+
(typeof viewMessage?.text === 'string' && viewMessage.text.length > 800);
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
agentId,
|
|
108
|
+
sessionId: agentId,
|
|
109
|
+
...(title ? { title } : {}),
|
|
110
|
+
...(status ? { status } : {}),
|
|
111
|
+
...(rawMessage
|
|
112
|
+
? {
|
|
113
|
+
message: rawMessage,
|
|
114
|
+
...(messageTruncated ? { messageTruncated: true } : {})
|
|
115
|
+
}
|
|
116
|
+
: {})
|
|
117
|
+
}
|
|
118
|
+
];
|
|
86
119
|
})
|
|
87
120
|
.slice(0, 100);
|
|
88
121
|
// An any-wait may return one result for several requested targets; keep the call multi-target.
|
|
@@ -90,12 +123,67 @@ export function agentOperationEvent(action, input, result) {
|
|
|
90
123
|
? operation.targets.map((target) => returnedTargets.find((result) => result.agentId === target.agentId) ?? target)
|
|
91
124
|
: returnedTargets;
|
|
92
125
|
const presentation = { ...operation, targets };
|
|
93
|
-
|
|
126
|
+
const messageReceipt = isPlainRecord(value.message) ? value.message : undefined;
|
|
127
|
+
const messageResult = isPlainRecord(messageReceipt?.result) ? messageReceipt.result : undefined;
|
|
128
|
+
const auditReceipt = isPlainRecord(messageResult?.auditReceipt)
|
|
129
|
+
? messageResult.auditReceipt
|
|
130
|
+
: undefined;
|
|
131
|
+
const failures = [value.message, value.goal, value.approval, value.observation, value.start]
|
|
132
|
+
.filter((part) => isPlainRecord(part) &&
|
|
133
|
+
['unknown', 'failed', 'partial', 'not_attempted'].includes(String(part.status)))
|
|
134
|
+
.map((part) => (isPlainRecord(part) ? (text(part.code) ?? text(part.status)) : undefined))
|
|
135
|
+
.filter((part) => part !== undefined);
|
|
136
|
+
if (failures.length)
|
|
137
|
+
presentation.detail = {
|
|
138
|
+
...presentation.detail,
|
|
139
|
+
failed: true,
|
|
140
|
+
error: failures.join('\n').slice(0, 500)
|
|
141
|
+
};
|
|
142
|
+
if (action === 'send' &&
|
|
143
|
+
message &&
|
|
144
|
+
failures.length === 0 &&
|
|
145
|
+
messageReceipt?.status === 'accepted') {
|
|
146
|
+
const delivery = messageResult?.delivery === 'QUEUED' || auditReceipt?.inputMode === 'QUEUE'
|
|
147
|
+
? 'QUEUE'
|
|
148
|
+
: auditReceipt?.inputMode === 'REPLACE_CURRENT'
|
|
149
|
+
? 'STEER'
|
|
150
|
+
: undefined;
|
|
151
|
+
if (delivery)
|
|
152
|
+
presentation.detail = { ...presentation.detail, delivery };
|
|
153
|
+
}
|
|
154
|
+
if (!presentation.detail &&
|
|
155
|
+
((action === 'list' && Array.isArray(value.sessions)) ||
|
|
156
|
+
(action === 'view' && isPlainRecord(value.session)) ||
|
|
157
|
+
(action === 'wait' && records.length > 0) ||
|
|
158
|
+
(action === 'stop' && typeof value.agentId === 'string')))
|
|
159
|
+
presentation.detail = {};
|
|
160
|
+
if (value.truncated === true || records.length > returnedTargets.length)
|
|
94
161
|
presentation.targetsTruncated = true;
|
|
95
162
|
// Core accepts at most 8 KiB of explicit extension event data.
|
|
96
163
|
while (Buffer.byteLength(JSON.stringify({ agentOperation: presentation }), 'utf8') > 7000) {
|
|
97
|
-
targets.
|
|
98
|
-
|
|
164
|
+
const targetWithMessage = targets.findLast((target) => target.message);
|
|
165
|
+
if (targetWithMessage?.message) {
|
|
166
|
+
const shortened = text(targetWithMessage.message, Math.floor(targetWithMessage.message.length / 2));
|
|
167
|
+
if (shortened)
|
|
168
|
+
targetWithMessage.message = shortened;
|
|
169
|
+
else
|
|
170
|
+
delete targetWithMessage.message;
|
|
171
|
+
targetWithMessage.messageTruncated = true;
|
|
172
|
+
}
|
|
173
|
+
else if (presentation.detail?.message) {
|
|
174
|
+
const shortened = text(presentation.detail.message, Math.floor(presentation.detail.message.length / 2));
|
|
175
|
+
if (shortened)
|
|
176
|
+
presentation.detail.message = shortened;
|
|
177
|
+
else
|
|
178
|
+
delete presentation.detail.message;
|
|
179
|
+
presentation.detail.messageTruncated = true;
|
|
180
|
+
}
|
|
181
|
+
else if (targets.length) {
|
|
182
|
+
targets.pop();
|
|
183
|
+
presentation.targetsTruncated = true;
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
break;
|
|
99
187
|
}
|
|
100
188
|
const receipt = {};
|
|
101
189
|
if (action === 'list' && Array.isArray(value.models)) {
|
|
@@ -1059,11 +1059,24 @@ function mergeAgentOperationData(toolName, previous, current) {
|
|
|
1059
1059
|
...record(current),
|
|
1060
1060
|
agentOperation: {
|
|
1061
1061
|
...after.data,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1062
|
+
...(after.data.detail === undefined && before.data.detail !== undefined
|
|
1063
|
+
? { detail: before.data.detail }
|
|
1064
|
+
: {}),
|
|
1065
|
+
targets: (after.data.action === 'WAIT'
|
|
1066
|
+
? [
|
|
1067
|
+
...after.data.targets,
|
|
1068
|
+
...before.data.targets.filter((target) => !after.data.targets.some((candidate) => candidate.agentId === target.agentId))
|
|
1069
|
+
]
|
|
1070
|
+
: after.data.targets).map((target) => {
|
|
1071
|
+
const resolved = before.data.targets.find((candidate) => candidate.agentId === target.agentId);
|
|
1072
|
+
const current = after.data.targets.find((candidate) => candidate.agentId === target.agentId);
|
|
1073
|
+
return {
|
|
1074
|
+
agentId: target.agentId,
|
|
1075
|
+
...(resolved?.sessionId ? { sessionId: resolved.sessionId } : {}),
|
|
1076
|
+
...(resolved?.title ? { title: resolved.title } : {}),
|
|
1077
|
+
...current
|
|
1078
|
+
};
|
|
1079
|
+
})
|
|
1067
1080
|
}
|
|
1068
1081
|
};
|
|
1069
1082
|
}
|
|
@@ -1213,7 +1226,10 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
1213
1226
|
];
|
|
1214
1227
|
}
|
|
1215
1228
|
const toolCategory = isImageGenerationTool(toolName) ? 'IMAGE_GENERATION' : undefined;
|
|
1216
|
-
const
|
|
1229
|
+
const readOutput = toolName === 'read' && status === 'COMPLETED' && outputSummary !== null
|
|
1230
|
+
? publicToolOutput(outputSummary, summaryTruncated ? 'SOURCE' : outputTruncation)
|
|
1231
|
+
: undefined;
|
|
1232
|
+
const presentation = projectToolPresentation(toolName, input, output, sleepStartedAt, outputSummary, readOutput);
|
|
1217
1233
|
const inputSummary = compactRunnerValue(input, 10_000);
|
|
1218
1234
|
return {
|
|
1219
1235
|
itemId: id,
|
|
@@ -1224,8 +1240,12 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
1224
1240
|
toolName: compactRunnerText(toolName, 128) || 'unknown',
|
|
1225
1241
|
title: compactRunnerText(toolName, 500) || 'unknown',
|
|
1226
1242
|
inputSummary: inputSummary.text || null,
|
|
1227
|
-
outputSummary: typeof output === 'string' || errorCode !== null ? null : outputSummary,
|
|
1228
|
-
...(
|
|
1243
|
+
outputSummary: typeof output === 'string' || errorCode !== null || readOutput ? null : outputSummary,
|
|
1244
|
+
...(readOutput
|
|
1245
|
+
? { output: readOutput }
|
|
1246
|
+
: typeof output === 'string'
|
|
1247
|
+
? { output: publicToolOutput(output, outputTruncation) }
|
|
1248
|
+
: {}),
|
|
1229
1249
|
...(typeof output !== 'string' && outputSummary !== null && summaryTruncated
|
|
1230
1250
|
? { outputSummaryTruncated: true }
|
|
1231
1251
|
: {}),
|
|
@@ -1243,7 +1263,7 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
1243
1263
|
merge: true
|
|
1244
1264
|
};
|
|
1245
1265
|
}
|
|
1246
|
-
function projectToolPresentation(toolName, input, output, startedAt) {
|
|
1266
|
+
function projectToolPresentation(toolName, input, output, startedAt, readSummary, readOutput) {
|
|
1247
1267
|
if (toolName === 'read') {
|
|
1248
1268
|
const reads = record(input)?.reads;
|
|
1249
1269
|
const paths = (Array.isArray(reads) ? reads : [])
|
|
@@ -1252,8 +1272,88 @@ function projectToolPresentation(toolName, input, output, startedAt) {
|
|
|
1252
1272
|
return typeof path === 'string' && path.length > 0 ? [compactRunnerText(path, 4096)] : [];
|
|
1253
1273
|
})
|
|
1254
1274
|
.slice(0, 16);
|
|
1255
|
-
|
|
1275
|
+
const data = record(output);
|
|
1276
|
+
if (data?.mode !== 'batch' ||
|
|
1277
|
+
!Array.isArray(data.results) ||
|
|
1278
|
+
readSummary == null ||
|
|
1279
|
+
!readOutput)
|
|
1280
|
+
return { kind: 'READ', paths };
|
|
1281
|
+
const text = readOutput.blocks.find((block) => block.channel === 'TEXT')?.text ?? '';
|
|
1282
|
+
const removed = readSummary.length - text.length;
|
|
1283
|
+
const results = data.results.slice(0, 16).flatMap((value) => {
|
|
1284
|
+
const item = record(value);
|
|
1285
|
+
if (typeof item?.path !== 'string' || !item.path)
|
|
1286
|
+
return [];
|
|
1287
|
+
const range = record(item.contentRange);
|
|
1288
|
+
const start = range?.start;
|
|
1289
|
+
const end = range?.end;
|
|
1290
|
+
const validRange = typeof start === 'number' &&
|
|
1291
|
+
Number.isSafeInteger(start) &&
|
|
1292
|
+
start >= 0 &&
|
|
1293
|
+
typeof end === 'number' &&
|
|
1294
|
+
Number.isSafeInteger(end) &&
|
|
1295
|
+
start <= end &&
|
|
1296
|
+
end <= readSummary.length &&
|
|
1297
|
+
(end > removed || (start === end && start >= removed));
|
|
1298
|
+
const error = record(item.error);
|
|
1299
|
+
return [
|
|
1300
|
+
{
|
|
1301
|
+
path: compactRunnerText(item.path, 4096),
|
|
1302
|
+
...(item.startLine === undefined ? {} : { startLine: item.startLine }),
|
|
1303
|
+
...(item.endLine === undefined ? {} : { endLine: item.endLine }),
|
|
1304
|
+
...(item.empty === true ? { empty: true } : {}),
|
|
1305
|
+
...(typeof error?.code === 'string' && typeof error.message === 'string'
|
|
1306
|
+
? {
|
|
1307
|
+
error: {
|
|
1308
|
+
code: compactRunnerText(error.code, 80),
|
|
1309
|
+
message: compactRunnerText(error.message, 1000)
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
: {}),
|
|
1313
|
+
...(validRange
|
|
1314
|
+
? {
|
|
1315
|
+
contentRange: {
|
|
1316
|
+
channel: 'TEXT',
|
|
1317
|
+
start: Math.max(start, removed) - removed,
|
|
1318
|
+
end: end - removed
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
: {})
|
|
1322
|
+
}
|
|
1323
|
+
];
|
|
1324
|
+
});
|
|
1325
|
+
const parsed = toolPresentationSchema.safeParse({ kind: 'READ', paths, results });
|
|
1326
|
+
return parsed.success ? parsed.data : { kind: 'READ', paths };
|
|
1327
|
+
}
|
|
1328
|
+
const args = record(input) ?? {};
|
|
1329
|
+
const data = record(output) ?? {};
|
|
1330
|
+
if (toolName === 'view_image') {
|
|
1331
|
+
const parsed = toolPresentationSchema.safeParse({
|
|
1332
|
+
kind: 'IMAGE_VIEW',
|
|
1333
|
+
path: typeof args.path === 'string' ? compactRunnerText(args.path, 4096) : undefined,
|
|
1334
|
+
resourceId: typeof args.resourceId === 'string' ? compactRunnerText(args.resourceId, 512) : undefined,
|
|
1335
|
+
resize: args.resize,
|
|
1336
|
+
mimeType: data.mimeType,
|
|
1337
|
+
width: data.width,
|
|
1338
|
+
height: data.height,
|
|
1339
|
+
sourceWidth: data.sourceWidth,
|
|
1340
|
+
sourceHeight: data.sourceHeight,
|
|
1341
|
+
region: data.region
|
|
1342
|
+
});
|
|
1343
|
+
return parsed.success
|
|
1344
|
+
? Object.fromEntries(Object.entries(parsed.data).filter(([, value]) => value !== undefined))
|
|
1345
|
+
: { kind: 'IMAGE_VIEW' };
|
|
1346
|
+
}
|
|
1347
|
+
if (toolName === 'tool_search') {
|
|
1348
|
+
const parsed = toolPresentationSchema.safeParse({
|
|
1349
|
+
kind: 'TOOL_SEARCH',
|
|
1350
|
+
...(typeof args.query === 'string' ? { query: compactRunnerText(args.query, 500) } : {}),
|
|
1351
|
+
...(data.tools === undefined ? {} : { tools: data.tools })
|
|
1352
|
+
});
|
|
1353
|
+
return parsed.success ? parsed.data : { kind: 'TOOL_SEARCH' };
|
|
1256
1354
|
}
|
|
1355
|
+
if (toolName === 'smart_compact')
|
|
1356
|
+
return { kind: 'COMPACT_REQUEST', ...(data.status === 'accepted' ? { accepted: true } : {}) };
|
|
1257
1357
|
const category = toolName === 'web_search'
|
|
1258
1358
|
? 'WEB_SEARCH'
|
|
1259
1359
|
: toolName === 'web_fetch'
|
|
@@ -1265,8 +1365,6 @@ function projectToolPresentation(toolName, input, output, startedAt) {
|
|
|
1265
1365
|
: undefined;
|
|
1266
1366
|
if (category === undefined)
|
|
1267
1367
|
return undefined;
|
|
1268
|
-
const args = record(input) ?? {};
|
|
1269
|
-
const data = record(output) ?? {};
|
|
1270
1368
|
// A provider-hosted search tool can report a page-read action under its native name.
|
|
1271
1369
|
const kind = category === 'WEB_SEARCH' && data.hostedToolKind === 'WEB_FETCH' ? 'WEB_FETCH' : category;
|
|
1272
1370
|
const text = (value, limit) => typeof value === 'string' ? compactRunnerText(value, limit) : undefined;
|
|
@@ -2,6 +2,21 @@ import { boundToolOutput } from '@myagentroam/protocol';
|
|
|
2
2
|
export function compactRunnerText(value, limit) {
|
|
3
3
|
return value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 1))}…`;
|
|
4
4
|
}
|
|
5
|
+
/** The current native call's message, never a target's result or inferred history. */
|
|
6
|
+
export function subagentCallDetail(value) {
|
|
7
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
8
|
+
return {};
|
|
9
|
+
let message = value.slice(0, 2000);
|
|
10
|
+
const last = message.charCodeAt(message.length - 1);
|
|
11
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
12
|
+
message = message.slice(0, -1);
|
|
13
|
+
return {
|
|
14
|
+
subagentCallDetail: {
|
|
15
|
+
message,
|
|
16
|
+
...(message.length < value.length ? { messageTruncated: true } : {})
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
5
20
|
export function publicOutputSnapshot(value, truncated = false) {
|
|
6
21
|
const text = typeof value === 'string' ? value : compactRunnerValue(value, Number.MAX_SAFE_INTEGER).text;
|
|
7
22
|
let bounded = text.slice(-10_000);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compactRunnerText, compactRunnerValue, publicToolOutput } from '../normalization.js';
|
|
1
|
+
import { compactRunnerText, compactRunnerValue, publicToolOutput, subagentCallDetail } from '../normalization.js';
|
|
2
2
|
import { shellExecution } from '../execution-presentation.js';
|
|
3
3
|
export function normalizeOpenCodeTool(toolName, state) {
|
|
4
4
|
const status = state?.status === 'completed'
|
|
@@ -53,6 +53,23 @@ export function normalizeOpenCodeTool(toolName, state) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
: {}),
|
|
56
|
+
...(normalizedName === 'webfetch' && typeof input?.url === 'string'
|
|
57
|
+
? {
|
|
58
|
+
toolPresentation: {
|
|
59
|
+
kind: 'WEB_FETCH',
|
|
60
|
+
detailOnly: true,
|
|
61
|
+
requestUrl: compactRunnerText(input.url, 8192)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
: normalizedName === 'websearch' && typeof input?.query === 'string'
|
|
65
|
+
? {
|
|
66
|
+
toolPresentation: {
|
|
67
|
+
kind: 'WEB_SEARCH',
|
|
68
|
+
detailOnly: true,
|
|
69
|
+
query: compactRunnerText(input.query, 500)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
56
73
|
...(typeof state?.output === 'string' ? { output: publicToolOutput(state.output) } : {}),
|
|
57
74
|
...subagent
|
|
58
75
|
}
|
|
@@ -72,6 +89,7 @@ function openCodeSubagentProjection(toolName, input, output) {
|
|
|
72
89
|
const title = typeof inputData?.description === 'string' ? compactRunnerText(inputData.description, 500) : '';
|
|
73
90
|
return {
|
|
74
91
|
subagentOperation: taskId === undefined ? 'START' : 'RESUME',
|
|
92
|
+
...(taskId === undefined ? subagentCallDetail(inputData?.prompt) : {}),
|
|
75
93
|
subagentId: taskId ?? nativeResult?.taskId ?? null,
|
|
76
94
|
...(title.length === 0 ? {} : { title }),
|
|
77
95
|
...(nativeResult === undefined
|
|
@@ -31,13 +31,16 @@ export class NativeSessionWatchService {
|
|
|
31
31
|
typeof input.watchId !== 'string' ||
|
|
32
32
|
typeof input.workbenchConnectionId !== 'string' ||
|
|
33
33
|
(input.mode !== 'initial' && input.mode !== 'renew') ||
|
|
34
|
+
(input.position !== undefined &&
|
|
35
|
+
input.position !== 'primary' &&
|
|
36
|
+
input.position !== 'secondary') ||
|
|
34
37
|
(input.unit !== undefined && input.unit !== 'SEGMENT'))
|
|
35
38
|
throw new Error('SESSION_INVALID');
|
|
36
39
|
const ownerKey = this.ownerKey(input);
|
|
37
40
|
if (input.mode === 'renew') {
|
|
38
41
|
const lease = this.leases.get(ownerKey);
|
|
39
42
|
if (lease === undefined ||
|
|
40
|
-
!this.sessionWatchSlots.owns(
|
|
43
|
+
!this.sessionWatchSlots.owns(lease.slotOwnerKey, lease.slotToken) ||
|
|
41
44
|
lease.watchId !== input.watchId ||
|
|
42
45
|
lease.sessionId !== input.sessionId ||
|
|
43
46
|
(input.workspaceId !== undefined && lease.workspaceId !== input.workspaceId))
|
|
@@ -57,7 +60,7 @@ export class NativeSessionWatchService {
|
|
|
57
60
|
const existing = this.leases.get(ownerKey);
|
|
58
61
|
const unit = input.unit === 'SEGMENT' ? 'SEGMENT' : 'TURN';
|
|
59
62
|
if (existing?.watchId === input.watchId &&
|
|
60
|
-
this.sessionWatchSlots.owns(
|
|
63
|
+
this.sessionWatchSlots.owns(existing.slotOwnerKey, existing.slotToken) &&
|
|
61
64
|
existing.sessionId === input.sessionId &&
|
|
62
65
|
(input.workspaceId === undefined || existing.workspaceId === input.workspaceId) &&
|
|
63
66
|
existing.unit === unit &&
|
|
@@ -68,9 +71,12 @@ export class NativeSessionWatchService {
|
|
|
68
71
|
return this.accepted(existing);
|
|
69
72
|
}
|
|
70
73
|
this.removeLease(ownerKey);
|
|
71
|
-
const
|
|
74
|
+
const slotOwnerKey = this.slotOwnerKey(input);
|
|
75
|
+
const position = input.position === 'secondary' ? 'secondary' : 'primary';
|
|
76
|
+
const slotToken = this.sessionWatchSlots.replace(slotOwnerKey, 'session', () => this.removeLease(ownerKey), position);
|
|
72
77
|
const lease = {
|
|
73
78
|
ownerKey,
|
|
79
|
+
slotOwnerKey,
|
|
74
80
|
sessionHash: this.sessionHash(input),
|
|
75
81
|
workbenchConnectionId: input.workbenchConnectionId,
|
|
76
82
|
watchId: input.watchId,
|
|
@@ -96,7 +102,10 @@ export class NativeSessionWatchService {
|
|
|
96
102
|
executeUnwatch(input) {
|
|
97
103
|
if (typeof input.sessionId !== 'string' ||
|
|
98
104
|
typeof input.watchId !== 'string' ||
|
|
99
|
-
typeof input.workbenchConnectionId !== 'string'
|
|
105
|
+
typeof input.workbenchConnectionId !== 'string' ||
|
|
106
|
+
(input.position !== undefined &&
|
|
107
|
+
input.position !== 'primary' &&
|
|
108
|
+
input.position !== 'secondary'))
|
|
100
109
|
throw new Error('SESSION_INVALID');
|
|
101
110
|
const ownerKey = this.ownerKey(input);
|
|
102
111
|
const lease = this.leases.get(ownerKey);
|
|
@@ -182,14 +191,13 @@ export class NativeSessionWatchService {
|
|
|
182
191
|
return this.watches.has(sessionId);
|
|
183
192
|
}
|
|
184
193
|
observes(sessionHash, connectionId, workspaceId, sessionId) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
return (lease !== undefined &&
|
|
194
|
+
const slotOwnerKey = `${sessionHash}:${connectionId}`;
|
|
195
|
+
return [...this.leases.values()].some((lease) => lease.slotOwnerKey === slotOwnerKey &&
|
|
188
196
|
lease.ready &&
|
|
189
197
|
lease.expiresAt > Date.now() &&
|
|
190
198
|
lease.sessionId === sessionId &&
|
|
191
199
|
lease.workspaceId === workspaceId &&
|
|
192
|
-
this.sessionWatchSlots.owns(
|
|
200
|
+
this.sessionWatchSlots.owns(slotOwnerKey, lease.slotToken));
|
|
193
201
|
}
|
|
194
202
|
isWatching(sessionId) {
|
|
195
203
|
return this.has(sessionId);
|
|
@@ -286,6 +294,9 @@ export class NativeSessionWatchService {
|
|
|
286
294
|
: 'local-test';
|
|
287
295
|
}
|
|
288
296
|
ownerKey(input) {
|
|
297
|
+
return `${this.slotOwnerKey(input)}:${input.position === 'secondary' ? 'secondary' : 'primary'}`;
|
|
298
|
+
}
|
|
299
|
+
slotOwnerKey(input) {
|
|
289
300
|
return `${this.sessionHash(input)}:${input.workbenchConnectionId}`;
|
|
290
301
|
}
|
|
291
302
|
accepted(lease) {
|
|
@@ -378,7 +389,7 @@ export class NativeSessionWatchService {
|
|
|
378
389
|
return;
|
|
379
390
|
clearTimeout(lease.timer);
|
|
380
391
|
this.leases.delete(ownerKey);
|
|
381
|
-
this.sessionWatchSlots.release(
|
|
392
|
+
this.sessionWatchSlots.release(lease.slotOwnerKey, lease.slotToken);
|
|
382
393
|
const observer = this.watches.get(lease.sessionId);
|
|
383
394
|
const nextExpiry = this.latestLeaseExpiry(lease.sessionId);
|
|
384
395
|
if (nextExpiry === 0)
|
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
/** Coordinates
|
|
1
|
+
/** Coordinates bounded ordinary positions and the mutually exclusive hosted watch. */
|
|
2
2
|
export class SessionWatchSlotCoordinator {
|
|
3
3
|
slots = new Map();
|
|
4
|
-
replace(ownerKey, kind, release) {
|
|
4
|
+
replace(ownerKey, kind, release, position = 'primary') {
|
|
5
|
+
const key = kind === 'session' ? position : 'hosted';
|
|
5
6
|
const previous = this.slots.get(ownerKey);
|
|
6
7
|
if (previous !== undefined) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
for (const [slotKey, slot] of previous) {
|
|
9
|
+
if (kind === 'hosted-conversation' || slotKey === 'hosted' || slotKey === key) {
|
|
10
|
+
previous.delete(slotKey);
|
|
11
|
+
slot.release();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
9
14
|
}
|
|
10
15
|
const token = Symbol(kind);
|
|
11
|
-
this.slots.
|
|
16
|
+
const slots = this.slots.get(ownerKey) ?? new Map();
|
|
17
|
+
slots.set(key, { kind, token, release });
|
|
18
|
+
this.slots.set(ownerKey, slots);
|
|
12
19
|
return token;
|
|
13
20
|
}
|
|
14
21
|
owns(ownerKey, token) {
|
|
15
|
-
return this.slots.get(ownerKey)?.token === token;
|
|
22
|
+
return [...(this.slots.get(ownerKey)?.values() ?? [])].some((slot) => slot.token === token);
|
|
16
23
|
}
|
|
17
24
|
release(ownerKey, token) {
|
|
18
|
-
|
|
25
|
+
const slots = this.slots.get(ownerKey);
|
|
26
|
+
if (slots === undefined)
|
|
27
|
+
return;
|
|
28
|
+
for (const [key, slot] of slots) {
|
|
29
|
+
if (slot.token === token)
|
|
30
|
+
slots.delete(key);
|
|
31
|
+
}
|
|
32
|
+
if (slots.size === 0)
|
|
19
33
|
this.slots.delete(ownerKey);
|
|
20
34
|
}
|
|
21
35
|
}
|
|
@@ -154,16 +154,18 @@ export class WebToolService {
|
|
|
154
154
|
} while (countModelTokens(content) > budget && metadataLimit >= 32);
|
|
155
155
|
if (countModelTokens(content) > budget)
|
|
156
156
|
throw invalidResult();
|
|
157
|
+
const data = {
|
|
158
|
+
query: parsed.data.query,
|
|
159
|
+
results: results.map((item, index) => ({
|
|
160
|
+
...item,
|
|
161
|
+
url: parsed.data.results[index].url
|
|
162
|
+
})),
|
|
163
|
+
truncated
|
|
164
|
+
};
|
|
157
165
|
return {
|
|
158
166
|
content,
|
|
159
|
-
data
|
|
160
|
-
|
|
161
|
-
results: results.map((item, index) => ({
|
|
162
|
-
...item,
|
|
163
|
-
url: parsed.data.results[index].url
|
|
164
|
-
})),
|
|
165
|
-
truncated
|
|
166
|
-
},
|
|
167
|
+
data,
|
|
168
|
+
eventData: { ...data, sourcesOnly: true },
|
|
167
169
|
truncated
|
|
168
170
|
};
|
|
169
171
|
}
|
|
@@ -48,6 +48,7 @@ export class WorkbenchManifestService {
|
|
|
48
48
|
},
|
|
49
49
|
directTransfer: capabilities.workspaceDirectTransfer,
|
|
50
50
|
terminalDirect: capabilities.terminalDirect,
|
|
51
|
+
sessionAside: capabilities.sessionAside,
|
|
51
52
|
sessionGroups: capabilities.sessionGroups,
|
|
52
53
|
git: {
|
|
53
54
|
available: true,
|
|
@@ -28,16 +28,27 @@ export class WorkspaceWatchController {
|
|
|
28
28
|
existing.topics.changes === topics.changes &&
|
|
29
29
|
JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
|
|
30
30
|
(existing.prepareFiles === true) === (input.prepareFiles === true);
|
|
31
|
+
const navigationOnly = existing !== undefined &&
|
|
32
|
+
existing.topics.sessions === topics.sessions &&
|
|
33
|
+
existing.topics.sessionGroup === topics.sessionGroup &&
|
|
34
|
+
existing.topics.changes === topics.changes &&
|
|
35
|
+
JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
|
|
36
|
+
!sameNavigation(existing.topics.sessionNavigation, topics.sessionNavigation) &&
|
|
37
|
+
(existing.prepareFiles === true) === (input.prepareFiles === true);
|
|
31
38
|
if (existing !== undefined &&
|
|
32
39
|
existing.watchId === input.watchId &&
|
|
33
|
-
|
|
40
|
+
existing.topics.sessions === topics.sessions &&
|
|
41
|
+
existing.topics.sessionGroup === topics.sessionGroup &&
|
|
42
|
+
existing.topics.changes === topics.changes &&
|
|
43
|
+
JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
|
|
44
|
+
sameNavigation(existing.topics.sessionNavigation, topics.sessionNavigation) &&
|
|
34
45
|
(existing.prepareFiles === true) === (input.prepareFiles === true)) {
|
|
35
46
|
this.options.watch.renew(workspace.id, key, input.watchId);
|
|
36
47
|
return this.accepted(existing);
|
|
37
48
|
}
|
|
38
49
|
const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics, input.prepareFiles === true);
|
|
39
|
-
void this.options.snapshots.initial(workspace, watch, groupOnly ? existing : undefined);
|
|
40
|
-
if (watch.prepareFiles)
|
|
50
|
+
void this.options.snapshots.initial(workspace, watch, groupOnly || navigationOnly ? existing : undefined);
|
|
51
|
+
if (watch.prepareFiles && !navigationOnly)
|
|
41
52
|
this.options.snapshots.prewarmFiles(workspace, watch);
|
|
42
53
|
return this.accepted(watch);
|
|
43
54
|
}
|
|
@@ -72,6 +83,16 @@ export class WorkspaceWatchController {
|
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
85
|
}
|
|
86
|
+
function sameNavigation(left, right) {
|
|
87
|
+
if (left === undefined || right === undefined)
|
|
88
|
+
return left === right;
|
|
89
|
+
return (left.limit === right.limit &&
|
|
90
|
+
left.treeAnchor === right.treeAnchor &&
|
|
91
|
+
left.sessionIds.length === right.sessionIds.length &&
|
|
92
|
+
left.sessionIds.every((id, index) => id === right.sessionIds[index]) &&
|
|
93
|
+
left.summaryAnchors.length === right.summaryAnchors.length &&
|
|
94
|
+
left.summaryAnchors.every((id, index) => id === right.summaryAnchors[index]));
|
|
95
|
+
}
|
|
75
96
|
function record(value) {
|
|
76
97
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
77
98
|
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
@@ -76,23 +76,104 @@ export class SessionCatalogService {
|
|
|
76
76
|
ancestors.add(parent.id);
|
|
77
77
|
root = parent;
|
|
78
78
|
}
|
|
79
|
+
const project = this.navigationProjector(input.workspaceId);
|
|
80
|
+
const queue = [root];
|
|
81
|
+
const visited = new Set([root.id]);
|
|
82
|
+
const nodes = [];
|
|
83
|
+
for (let index = 0; index < queue.length && index < limit; index += 1) {
|
|
84
|
+
const session = queue[index];
|
|
85
|
+
nodes.push(project(session));
|
|
86
|
+
const children = [...database.listChildSessionRecords(input.workspaceId, session.id)];
|
|
87
|
+
if (input.includeAncestors === true)
|
|
88
|
+
children.sort((left, right) => Number(ancestors.has(right.id)) - Number(ancestors.has(left.id)));
|
|
89
|
+
for (const child of children) {
|
|
90
|
+
if (visited.has(child.id))
|
|
91
|
+
continue;
|
|
92
|
+
visited.add(child.id);
|
|
93
|
+
queue.push(child);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
|
|
97
|
+
}
|
|
98
|
+
navigation(query) {
|
|
99
|
+
const database = this.options.database();
|
|
100
|
+
const project = this.navigationProjector(query.workspaceId);
|
|
101
|
+
const get = (id) => {
|
|
102
|
+
const record = database.getSessionRecord(id);
|
|
103
|
+
return record?.workspaceId === query.workspaceId ? record : undefined;
|
|
104
|
+
};
|
|
105
|
+
const requestedIds = new Set([
|
|
106
|
+
...query.sessionIds,
|
|
107
|
+
...query.summaryAnchors,
|
|
108
|
+
...(query.treeAnchor === undefined ? [] : [query.treeAnchor])
|
|
109
|
+
]);
|
|
110
|
+
const missingSessionIds = [...requestedIds].filter((id) => get(id) === undefined);
|
|
111
|
+
const sessions = query.sessionIds.flatMap((id) => {
|
|
112
|
+
const record = get(id);
|
|
113
|
+
return record === undefined ? [] : [project(record)];
|
|
114
|
+
});
|
|
115
|
+
const descendantsByAnchor = new Map();
|
|
116
|
+
const descendants = (anchor) => {
|
|
117
|
+
let records = descendantsByAnchor.get(anchor);
|
|
118
|
+
if (records === undefined) {
|
|
119
|
+
records = database.sessionTreeRecords(query.workspaceId, anchor).slice(1);
|
|
120
|
+
descendantsByAnchor.set(anchor, records);
|
|
121
|
+
}
|
|
122
|
+
return records;
|
|
123
|
+
};
|
|
124
|
+
const summaries = query.summaryAnchors.flatMap((anchorSessionId) => {
|
|
125
|
+
if (get(anchorSessionId) === undefined)
|
|
126
|
+
return [];
|
|
127
|
+
let runningCount = 0;
|
|
128
|
+
let sleepingCount = 0;
|
|
129
|
+
const records = descendants(anchorSessionId);
|
|
130
|
+
for (const record of records) {
|
|
131
|
+
const status = sessionActivityStatus(this.options.runtime, record.id);
|
|
132
|
+
if (status === 'RUNNING')
|
|
133
|
+
runningCount++;
|
|
134
|
+
else if (status === 'SLEEPING')
|
|
135
|
+
sleepingCount++;
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
anchorSessionId,
|
|
140
|
+
descendantCount: records.length,
|
|
141
|
+
activeCount: runningCount + sleepingCount,
|
|
142
|
+
runningCount,
|
|
143
|
+
sleepingCount,
|
|
144
|
+
hasActiveDescendants: runningCount + sleepingCount > 0
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
});
|
|
148
|
+
const anchor = query.treeAnchor === undefined ? undefined : get(query.treeAnchor);
|
|
149
|
+
const records = anchor === undefined ? [] : descendants(anchor.id);
|
|
150
|
+
const parent = anchor?.metadata.parentSessionId
|
|
151
|
+
? get(anchor.metadata.parentSessionId)
|
|
152
|
+
: undefined;
|
|
153
|
+
const tree = anchor === undefined
|
|
154
|
+
? null
|
|
155
|
+
: {
|
|
156
|
+
anchorSessionId: anchor.id,
|
|
157
|
+
parent: parent === undefined ? null : project(parent),
|
|
158
|
+
nodes: records.slice(0, query.limit).map(project),
|
|
159
|
+
truncated: records.length > query.limit
|
|
160
|
+
};
|
|
161
|
+
return { sessions, summaries, tree, missingSessionIds };
|
|
162
|
+
}
|
|
163
|
+
navigationProjector(workspaceId) {
|
|
79
164
|
const native = this.options
|
|
80
165
|
.cachedNative()
|
|
81
|
-
.filter((session) => session.workspaceId ===
|
|
166
|
+
.filter((session) => session.workspaceId === workspaceId);
|
|
82
167
|
const projections = this.options.projection.projectionsForNative(native);
|
|
83
168
|
const activityById = new Map();
|
|
84
169
|
for (const source of native) {
|
|
85
170
|
const id = projections.get(source)?.id ?? source.id;
|
|
86
171
|
activityById.set(id, latestSessionActivityAt(activityById.get(id) ?? 0, source.lastActivityAt));
|
|
87
172
|
}
|
|
88
|
-
|
|
89
|
-
const visited = new Set([root.id]);
|
|
90
|
-
const nodes = [];
|
|
91
|
-
for (let index = 0; index < queue.length && index < limit; index += 1) {
|
|
92
|
-
const session = queue[index];
|
|
173
|
+
return (session) => {
|
|
93
174
|
const active = this.options.runtime.getAgentSession(session.id);
|
|
94
175
|
const live = active === undefined ? undefined : this.options.present(active);
|
|
95
|
-
|
|
176
|
+
return {
|
|
96
177
|
id: session.id,
|
|
97
178
|
parentSessionId: session.metadata.parentSessionId ?? null,
|
|
98
179
|
title: live?.customTitle ??
|
|
@@ -104,18 +185,8 @@ export class SessionCatalogService {
|
|
|
104
185
|
activity: live && 'activity' in live ? live.activity : 'IDLE',
|
|
105
186
|
status: sessionActivityStatus(this.options.runtime, session.id),
|
|
106
187
|
lastActivityAt: latestSessionActivityAt(live?.lastActivityAt ?? 0, activityById.get(session.id) ?? 0)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (input.includeAncestors === true)
|
|
110
|
-
children.sort((left, right) => Number(ancestors.has(right.id)) - Number(ancestors.has(left.id)));
|
|
111
|
-
for (const child of children) {
|
|
112
|
-
if (visited.has(child.id))
|
|
113
|
-
continue;
|
|
114
|
-
visited.add(child.id);
|
|
115
|
-
queue.push(child);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
|
|
188
|
+
};
|
|
189
|
+
};
|
|
119
190
|
}
|
|
120
191
|
async collect(input) {
|
|
121
192
|
const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
|
+
import { workspaceSessionNavigationQuerySchema } from '@myagentroam/protocol';
|
|
2
3
|
const WATCH_TTL_MS = 45_000;
|
|
3
4
|
export class WorkspaceWatchService {
|
|
4
5
|
onSessionWatchRequired;
|
|
@@ -22,6 +23,14 @@ export class WorkspaceWatchService {
|
|
|
22
23
|
if (sessionGroup !== undefined && sessionGroup !== 'MAIN' && sessionGroup !== 'CHILD')
|
|
23
24
|
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
24
25
|
const changes = input.topics.changes === true;
|
|
26
|
+
const rawNavigation = input.topics.sessionNavigation;
|
|
27
|
+
let sessionNavigation;
|
|
28
|
+
if (rawNavigation !== undefined) {
|
|
29
|
+
const parsed = workspaceSessionNavigationQuerySchema.safeParse(rawNavigation);
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
32
|
+
sessionNavigation = parsed.data;
|
|
33
|
+
}
|
|
25
34
|
const rawFiles = input.topics.files;
|
|
26
35
|
let files;
|
|
27
36
|
if (rawFiles !== undefined) {
|
|
@@ -40,9 +49,15 @@ export class WorkspaceWatchService {
|
|
|
40
49
|
...(typeof rawFiles.limit === 'number' ? { limit: rawFiles.limit } : {})
|
|
41
50
|
};
|
|
42
51
|
}
|
|
43
|
-
if (!sessions && !changes && files === undefined)
|
|
52
|
+
if (!sessions && !changes && files === undefined && sessionNavigation === undefined)
|
|
44
53
|
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
45
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
sessions,
|
|
56
|
+
changes,
|
|
57
|
+
files,
|
|
58
|
+
...(sessionGroup === undefined ? {} : { sessionGroup }),
|
|
59
|
+
...(sessionNavigation === undefined ? {} : { sessionNavigation })
|
|
60
|
+
};
|
|
46
61
|
}
|
|
47
62
|
key(input) {
|
|
48
63
|
return `${this.sessionHash(input)}:${this.connectionId(input)}`;
|
|
@@ -81,10 +96,11 @@ export class WorkspaceWatchService {
|
|
|
81
96
|
if (previous !== undefined) {
|
|
82
97
|
clearTimeout(previous.timer);
|
|
83
98
|
this.onWatchRemoved(previous);
|
|
84
|
-
if (previous.topics.sessions
|
|
99
|
+
if ((previous.topics.sessions || previous.topics.sessionNavigation !== undefined) &&
|
|
100
|
+
!this.topicActive(workspaceId, 'sessions'))
|
|
85
101
|
this.onSessionWatchUnused(workspaceId);
|
|
86
102
|
}
|
|
87
|
-
if (topics.sessions)
|
|
103
|
+
if (topics.sessions || topics.sessionNavigation !== undefined)
|
|
88
104
|
this.onSessionWatchRequired(workspaceId);
|
|
89
105
|
return watch;
|
|
90
106
|
}
|
|
@@ -167,7 +183,11 @@ export class WorkspaceWatchService {
|
|
|
167
183
|
return this.watchSnapshotSequence;
|
|
168
184
|
}
|
|
169
185
|
topicActive(workspaceId, topic) {
|
|
170
|
-
return [...(this.watches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files'
|
|
186
|
+
return [...(this.watches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files'
|
|
187
|
+
? watch.topics.files !== undefined
|
|
188
|
+
: topic === 'sessions'
|
|
189
|
+
? watch.topics.sessions || watch.topics.sessionNavigation !== undefined
|
|
190
|
+
: watch.topics[topic]);
|
|
171
191
|
}
|
|
172
192
|
activeWatches(workspaceId) {
|
|
173
193
|
return [...(this.watches.get(workspaceId)?.values() ?? [])].filter((watch) => watch.expiresAt > Date.now());
|
|
@@ -20,7 +20,9 @@ export class WorkspaceWatchSnapshotService {
|
|
|
20
20
|
await Promise.all(this.subscribedTopics(watch).map(async (topic) => {
|
|
21
21
|
const revision = this.readRevision(workspace.id, topic);
|
|
22
22
|
try {
|
|
23
|
-
const retained = topic === '
|
|
23
|
+
const retained = topic === 'sessionNavigation' ||
|
|
24
|
+
previous === undefined ||
|
|
25
|
+
(topic === 'sessions' && previous.topics.sessionGroup !== watch.topics.sessionGroup)
|
|
24
26
|
? undefined
|
|
25
27
|
: this.resourceSnapshots.get(previous)?.[topic];
|
|
26
28
|
const payload = retained !== undefined &&
|
|
@@ -35,7 +37,8 @@ export class WorkspaceWatchSnapshotService {
|
|
|
35
37
|
void this.invalidate(workspace.id, topic);
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
|
-
|
|
40
|
+
if (topic !== 'sessionNavigation')
|
|
41
|
+
this.signatures.set(this.viewKey(workspace.id, topic, watch), signature(payload));
|
|
39
42
|
this.deliver(workspace, watch, topic, payload);
|
|
40
43
|
}
|
|
41
44
|
catch {
|
|
@@ -64,6 +67,8 @@ export class WorkspaceWatchSnapshotService {
|
|
|
64
67
|
this.filePrewarmTimers.delete(watch);
|
|
65
68
|
}
|
|
66
69
|
async invalidate(workspaceId, topic) {
|
|
70
|
+
if (topic === 'sessions')
|
|
71
|
+
void this.invalidate(workspaceId, 'sessionNavigation');
|
|
67
72
|
const key = `${workspaceId}:${topic}`;
|
|
68
73
|
const active = this.refreshes.get(key);
|
|
69
74
|
if (active !== undefined) {
|
|
@@ -134,6 +139,18 @@ export class WorkspaceWatchSnapshotService {
|
|
|
134
139
|
const subscribers = this.subscribers(workspaceId, topic);
|
|
135
140
|
if (subscribers.length === 0)
|
|
136
141
|
return;
|
|
142
|
+
if (topic === 'sessionNavigation') {
|
|
143
|
+
await Promise.all(subscribers.map(async (watch) => {
|
|
144
|
+
try {
|
|
145
|
+
const payload = await this.read(workspace, topic, watch);
|
|
146
|
+
this.deliver(workspace, watch, topic, payload);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
this.fail(workspaceId, watch, topic);
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
137
154
|
if (topic === 'changes') {
|
|
138
155
|
const first = subscribers[0];
|
|
139
156
|
if (first === undefined)
|
|
@@ -191,23 +208,28 @@ export class WorkspaceWatchSnapshotService {
|
|
|
191
208
|
}));
|
|
192
209
|
}
|
|
193
210
|
read(workspace, topic, watch) {
|
|
194
|
-
const key = topic === '
|
|
195
|
-
? `${workspace.id}:
|
|
196
|
-
:
|
|
211
|
+
const key = topic === 'sessionNavigation'
|
|
212
|
+
? `${workspace.id}:sessionNavigation:${watch.watchId}`
|
|
213
|
+
: topic === 'files'
|
|
214
|
+
? `${workspace.id}:files:${JSON.stringify(watch.topics.files)}`
|
|
215
|
+
: `${workspace.id}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}:${this.readRevision(workspace.id, topic)}`;
|
|
197
216
|
const active = this.reads.get(key);
|
|
198
217
|
if (active !== undefined)
|
|
199
218
|
return active;
|
|
200
|
-
const read = (topic === '
|
|
201
|
-
? this.options.
|
|
202
|
-
: topic === '
|
|
203
|
-
? workspace.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
?
|
|
207
|
-
:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
const read = (topic === 'sessionNavigation'
|
|
220
|
+
? this.options.sessionNavigation(workspace.id, watch.topics.sessionNavigation)
|
|
221
|
+
: topic === 'sessions'
|
|
222
|
+
? this.options.listSessions(workspace.id, watch.topics.sessionGroup)
|
|
223
|
+
: topic === 'changes'
|
|
224
|
+
? workspace.kind === 'GIT_WORKSPACE'
|
|
225
|
+
? this.options.changes.current(workspace)
|
|
226
|
+
: Promise.resolve(workspace.includeParentRepository === false
|
|
227
|
+
? { workspace, changes: [], repositories: [] }
|
|
228
|
+
: null)
|
|
229
|
+
: this.options.files.list(workspace, watch.topics.files).then((files) => ({
|
|
230
|
+
files,
|
|
231
|
+
workspace: this.options.requireWorkspace(workspace.id)
|
|
232
|
+
}))).finally(() => {
|
|
211
233
|
if (this.reads.get(key) === read)
|
|
212
234
|
this.reads.delete(key);
|
|
213
235
|
});
|
|
@@ -225,9 +247,11 @@ export class WorkspaceWatchSnapshotService {
|
|
|
225
247
|
return topic === 'changes' ? this.options.changes.revision(workspaceId) : 0;
|
|
226
248
|
}
|
|
227
249
|
viewKey(workspaceId, topic, watch) {
|
|
228
|
-
return topic === '
|
|
229
|
-
? `${workspaceId}:
|
|
230
|
-
:
|
|
250
|
+
return topic === 'sessionNavigation'
|
|
251
|
+
? `${workspaceId}:sessionNavigation:${watch.sessionHash}:${watch.workbenchConnectionId}`
|
|
252
|
+
: topic === 'files'
|
|
253
|
+
? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
|
|
254
|
+
: `${workspaceId}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}`;
|
|
231
255
|
}
|
|
232
256
|
readActive(workspaceId, topic, watch) {
|
|
233
257
|
if (topic === 'files' && watch !== undefined)
|
|
@@ -238,11 +262,16 @@ export class WorkspaceWatchSnapshotService {
|
|
|
238
262
|
subscribers(workspaceId, topic) {
|
|
239
263
|
return this.options.watch
|
|
240
264
|
.activeWatches(workspaceId)
|
|
241
|
-
.filter((watch) => topic === 'files'
|
|
265
|
+
.filter((watch) => topic === 'files'
|
|
266
|
+
? watch.topics.files !== undefined
|
|
267
|
+
: topic === 'sessionNavigation'
|
|
268
|
+
? watch.topics.sessionNavigation !== undefined
|
|
269
|
+
: watch.topics[topic]);
|
|
242
270
|
}
|
|
243
271
|
subscribedTopics(watch) {
|
|
244
272
|
return [
|
|
245
273
|
...(watch.topics.sessions ? ['sessions'] : []),
|
|
274
|
+
...(watch.topics.sessionNavigation === undefined ? [] : ['sessionNavigation']),
|
|
246
275
|
...(watch.topics.files === undefined ? [] : ['files']),
|
|
247
276
|
...(watch.topics.changes ? ['changes'] : [])
|
|
248
277
|
];
|
|
@@ -250,7 +279,7 @@ export class WorkspaceWatchSnapshotService {
|
|
|
250
279
|
deliver(workspace, watch, topic, payload) {
|
|
251
280
|
if (!this.matches(workspace.id, watch))
|
|
252
281
|
return;
|
|
253
|
-
if (topic !== '
|
|
282
|
+
if (topic !== 'sessionNavigation') {
|
|
254
283
|
const snapshots = this.resourceSnapshots.get(watch) ?? {};
|
|
255
284
|
snapshots[topic] = { payload, revision: this.readRevision(workspace.id, topic) };
|
|
256
285
|
this.resourceSnapshots.set(watch, snapshots);
|
|
@@ -272,7 +301,7 @@ export class WorkspaceWatchSnapshotService {
|
|
|
272
301
|
fail(workspaceId, watch, topic) {
|
|
273
302
|
if (!this.matches(workspaceId, watch))
|
|
274
303
|
return;
|
|
275
|
-
if (topic !== '
|
|
304
|
+
if (topic !== 'sessionNavigation') {
|
|
276
305
|
const snapshots = this.resourceSnapshots.get(watch);
|
|
277
306
|
if (snapshots !== undefined)
|
|
278
307
|
delete snapshots[topic];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.70",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"turndown-plugin-gfm": "^1.0.2",
|
|
41
41
|
"ws": "^8.21.3",
|
|
42
42
|
"zod": "4.4.3",
|
|
43
|
-
"@myagentroam/agent": "0.11.
|
|
43
|
+
"@myagentroam/agent": "0.11.70",
|
|
44
44
|
"@myagentroam/code-navigation": "0.11.1",
|
|
45
|
-
"@myagentroam/
|
|
46
|
-
"@myagentroam/
|
|
45
|
+
"@myagentroam/protocol": "0.11.70",
|
|
46
|
+
"@myagentroam/marctl": "0.11.12"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/turndown": "^5.0.6",
|