@myagentroam/node 0.10.20 → 0.10.22

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.
@@ -13,6 +13,7 @@ const runtimeCredentialCapability = () => ({
13
13
  namespacedTools: true,
14
14
  responsesOptionalParameters: true,
15
15
  responsesHttpContinuation: true,
16
+ conversationModels: true,
16
17
  httpVersions: ['1.1', '2']
17
18
  });
18
19
  const workspaceDirectTransferCapability = () => ({
@@ -56,6 +57,7 @@ export function unavailableCapabilities() {
56
57
  openCode: { available: false },
57
58
  marAgent: { available: true, version: nodeAppVersion },
58
59
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
60
+ promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
59
61
  chatgptLogin: { apiVersion: 1, loopback: true },
60
62
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
61
63
  terminalDirect: terminalDirectCapability()
@@ -91,6 +93,7 @@ export function detectCapabilities(commands = {}) {
91
93
  },
92
94
  marAgent: { available: true, version: nodeAppVersion },
93
95
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
96
+ promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
94
97
  chatgptLogin: { apiVersion: 1, loopback: true },
95
98
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
96
99
  terminalDirect: terminalDirectCapability()
@@ -130,6 +133,7 @@ export async function detectCapabilitiesAsync(commands = {}) {
130
133
  },
131
134
  marAgent: { available: true, version: nodeAppVersion },
132
135
  marAgentRuntimeCredentials: runtimeCredentialCapability(),
136
+ promptCompletion: { apiVersion: 1, contextVersion: 3, automaticTriggers: true },
133
137
  chatgptLogin: { apiVersion: 1, loopback: true },
134
138
  workspaceDirectTransfer: workspaceDirectTransferCapability(),
135
139
  terminalDirect: terminalDirectCapability()
package/dist/connector.js CHANGED
@@ -102,6 +102,8 @@ import { ComputerUseEnablementService } from './service/computer-use-enablement-
102
102
  import { safeErrorCode } from './util/safe-error.js';
103
103
  import { RuntimeCredentialControlService } from './service/runtime-credential-control-service.js';
104
104
  import { MarAgentRuntimeModelFactory } from './runner/mar-agent/runtime-model-factory.js';
105
+ import { PromptCompletionNodeService } from './service/prompt-completion-node-service.js';
106
+ import { PromptCompletionWorkspaceSource } from './service/prompt-completion-workspace-source.js';
105
107
  import { MarAgentModelTransportTestService } from './service/mar-agent-model-transport-test-service.js';
106
108
  import { ChatgptLoginService } from './service/chatgpt-login-service.js';
107
109
  const HEARTBEAT_MS = 15_000;
@@ -219,6 +221,7 @@ export class NodeConnector {
219
221
  sessionIdentityService;
220
222
  nativeSessionProjectionService;
221
223
  conversationHistoryService;
224
+ promptCompletion;
222
225
  conversationSegmentService;
223
226
  runnerInteractionService;
224
227
  runnerChannelMessages;
@@ -1191,13 +1194,17 @@ export class NodeConnector {
1191
1194
  nativeIds: (session) => this.nativeSessionProjectionService.nativeIds(session),
1192
1195
  forget: (session, nativeIds) => {
1193
1196
  this.nativeSessionProjectionService.forget(session, nativeIds);
1197
+ this.promptCompletion.invalidateSession(session.workspaceId, session.id);
1194
1198
  void this.runAttachmentService.releaseSession(session.id);
1195
1199
  },
1196
1200
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
1197
1201
  externalActivity: (sessionId) => this.sessionState.externalActivity.get(sessionId),
1198
1202
  isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
1199
1203
  history: (session, input) => this.conversationHistoryService.read(session, input),
1200
- invalidateHistory: (sessionId) => this.nativeSessionWatchService.invalidate(sessionId),
1204
+ invalidateHistory: (sessionId, workspaceId) => {
1205
+ this.nativeSessionWatchService.invalidate(sessionId);
1206
+ this.promptCompletion.invalidateSession(workspaceId, sessionId);
1207
+ },
1201
1208
  invalidateWorkspaceSessions: (workspaceId) => {
1202
1209
  this.workspaceSessionService.invalidateCache(workspaceId);
1203
1210
  void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
@@ -1297,6 +1304,7 @@ export class NodeConnector {
1297
1304
  onReconnectScheduled: () => this.metrics.reconnectScheduled(),
1298
1305
  onMessage: (raw) => this.handleMessage(raw),
1299
1306
  onClose: (code, reason) => {
1307
+ this.promptCompletion?.disconnect();
1300
1308
  this.usageOutbox?.disconnected();
1301
1309
  this.directTransferService?.close();
1302
1310
  this.runtimeCredentials.disconnected();
@@ -1477,6 +1485,7 @@ export class NodeConnector {
1477
1485
  this.workspaceCoordinator.invalidateChanges(workspaceId);
1478
1486
  this.workspaceSessionService.invalidate(workspaceId);
1479
1487
  this.workspaceCoordinator.clearWatches(workspaceId);
1488
+ this.promptCompletion?.invalidateWorkspace(workspaceId);
1480
1489
  });
1481
1490
  this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
1482
1491
  requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
@@ -1496,6 +1505,53 @@ export class NodeConnector {
1496
1505
  nodeGeneration: () => this.nodeGeneration,
1497
1506
  emitWatchEvent: (payload) => this.send('watch.event', payload)
1498
1507
  });
1508
+ const completionWorkspace = new PromptCompletionWorkspaceSource({
1509
+ requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
1510
+ observesWorkspace: (workspaceId, actor, connectionId) => this.workspaceWatchService.observes(workspaceId, actor.sessionHash, connectionId),
1511
+ observesSession: (workspaceId, sessionId, actor, connectionId) => this.nativeSessionWatchService.observes(actor.sessionHash, connectionId, workspaceId, sessionId),
1512
+ resolveSession: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
1513
+ cachedSessions: (workspaceId) => this.workspaceState.sessionCache.get(workspaceId)?.sessions ?? [],
1514
+ readHistory: (session, limit) => this.conversationHistoryService.read(session, { limit })
1515
+ });
1516
+ this.promptCompletion = new PromptCompletionNodeService({
1517
+ generation: () => this.nodeGeneration,
1518
+ readAttentionFile: (input, _actor, event) => {
1519
+ return this.workspaceWorkbenchService.readPredictionFile(input.target.workspaceId, event.path, event.worktreePath);
1520
+ },
1521
+ taskState: (input) => {
1522
+ const id = input.sessionId;
1523
+ if (!id || !this.runtime.getAgentSession(id))
1524
+ return { goal: 'UNKNOWN', queue: 'UNKNOWN' };
1525
+ const queue = this.runtime.sessionQueue(id);
1526
+ const run = this.runtime.activeSessionRun(id) ?? this.runtime.listRunsForSession(id)[0];
1527
+ const goal = this.goalRuntime.get(id);
1528
+ return {
1529
+ ...(run ? { runStatus: run.status } : {}),
1530
+ goal: goal
1531
+ ? {
1532
+ id: goal.id,
1533
+ objective: goal.objective,
1534
+ status: goal.status,
1535
+ revision: goal.revision
1536
+ }
1537
+ : null,
1538
+ queue: {
1539
+ version: queue.version,
1540
+ paused: queue.paused,
1541
+ items: queue.itemIds.flatMap((itemId) => {
1542
+ const item = this.runtime.getQueueItem(itemId);
1543
+ return item ? [{ queueItemId: item.id, content: item.content }] : [];
1544
+ })
1545
+ }
1546
+ };
1547
+ },
1548
+ modelFactory: this.marAgentRuntimeModels,
1549
+ usage: this.modelUsage,
1550
+ observed: (input, actor) => completionWorkspace.observed(input, actor),
1551
+ readPage: (input, actor, signal) => completionWorkspace.readPage(input, actor, signal),
1552
+ references: (workspaceId, _actor, signal) => completionWorkspace.references(workspaceId, signal)
1553
+ });
1554
+ this.operationRouter.registerAll(this.promptCompletion.operations());
1499
1555
  for (const operation of CONVERSATION_TEMPLATE_OPERATIONS)
1500
1556
  this.operationRouter.register(operation, (data) => {
1501
1557
  const handler = this.conversationTemplateOperations?.[operation];
@@ -1726,6 +1782,7 @@ export class NodeConnector {
1726
1782
  this.stopping = undefined;
1727
1783
  this.stopped = false;
1728
1784
  this.config ??= await loadNodeConfig(this.configPath);
1785
+ this.promptCompletion.initializeJournal(nodeDataDirectory(this.config, this.configPath));
1729
1786
  await this.upgradeCoordinator.loadResult(resolve(dirname(nodeDatabasePath(this.config, this.configPath)), 'upgrade-result.json'));
1730
1787
  await assertLocalSqlitePath(nodeDatabasePath(this.config, this.configPath));
1731
1788
  this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
@@ -2083,6 +2140,7 @@ export class NodeConnector {
2083
2140
  }
2084
2141
  this.claudeClient.execution.userInputs.clear();
2085
2142
  this.nativeSessionWatchService.clear();
2143
+ this.promptCompletion.dispose();
2086
2144
  this.sessionIdentityService.dispose();
2087
2145
  this.sessionLifecycleService.clear();
2088
2146
  this.continuousRunCoordinator.clear();
@@ -62,6 +62,7 @@ export class MarAgentRuntimeModelFactory {
62
62
  outputCapabilities: configuration.outputCapabilities,
63
63
  reasoningEfforts: configuration.reasoningEfforts,
64
64
  defaultReasoningEffort: resolveMarAgentDefaultReasoningEffort(configuration.defaultReasoningEffort, configuration.reasoningEfforts),
65
+ conversationModel: configuration.conversationModel ?? true,
65
66
  titleGeneration: configuration.titleGeneration,
66
67
  codeMode: configuration.codeMode,
67
68
  ...(configuration.serviceTier === undefined
@@ -95,7 +95,7 @@ export class MarAgentRunner extends AbstractRunner {
95
95
  return capabilities.marAgent?.available === true;
96
96
  }
97
97
  profileForValidation() {
98
- const models = this.#models.filter((model) => model.enabled);
98
+ const models = this.#models.filter((model) => model.enabled && model.conversationModel !== false);
99
99
  return {
100
100
  runner: 'mar-agent',
101
101
  models: models.map((model, index) => ({
@@ -259,8 +259,10 @@ export class HostedConversationNodeService {
259
259
  : this.latestAvailableTemplateVersionId(conversation.templateId);
260
260
  const displayVersionId = latestVersionId ?? conversation.activeVersionId;
261
261
  const version = await this.loadTemplateVersion(displayVersionId, conversation.templateId);
262
- const models = networkModels(allowedModels(version, availableModels), version.draft.networkMode);
263
- if (models.length > 0) {
262
+ const models = networkModels(allowedModels(version, availableModels), version.draft.networkMode).filter((model) => model.conversationModel !== false);
263
+ if (models.length > 0 &&
264
+ availableModels.find((model) => model.id === conversation.modelId)?.conversationModel !==
265
+ false) {
264
266
  const configuredModelId = conversation.modelId;
265
267
  const selected = selectModel(version, availableModels, conversation.modelId);
266
268
  if (selected.id !== conversation.modelId)
@@ -1707,10 +1709,12 @@ function allowedModels(version, models) {
1707
1709
  return models.filter((model) => model.enabled && allowed.has(model.modelId));
1708
1710
  }
1709
1711
  function selectModel(version, models, requestedModelId) {
1710
- const available = allowedModels(version, models);
1712
+ const available = allowedModels(version, models).filter((model) => model.conversationModel !== false);
1711
1713
  const requested = available.find((candidate) => candidate.id === requestedModelId);
1712
1714
  if (requested !== undefined)
1713
1715
  return requested;
1716
+ if (models.some((candidate) => candidate.id === requestedModelId && candidate.conversationModel === false))
1717
+ throw new Error('HOSTED_CONVERSATION_MODEL_NOT_AVAILABLE');
1714
1718
  const defaultModel = available.find((candidate) => candidate.modelId === version.modelPolicy.defaultId);
1715
1719
  if (defaultModel !== undefined)
1716
1720
  return defaultModel;
@@ -1,6 +1,7 @@
1
1
  import { modelUsageGrantSchema, modelUsageGrantClaimsSchema, modelUsageRecordSchema } from '@myagentroam/protocol';
2
2
  import { nodeLog } from '../operational.js';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { normalizeMarAgentUsage } from '../runner/mar-agent/conversation-normalizer.js';
4
5
  /** Execution-private attribution never enters Run projections or browser Watch events. */
5
6
  export class NodeModelUsageService {
6
7
  outbox;
@@ -27,7 +28,13 @@ export class NodeModelUsageService {
27
28
  const claims = modelUsageGrantClaimsSchema.parse(JSON.parse(Buffer.from(parsedGrant.token.split('.')[0], 'base64url').toString()));
28
29
  if (claims.nodeId !== this.nodeId() || claims.scopeId !== parsedGrant.scopeId)
29
30
  return;
30
- this.bindings.set(runId, { grant: parsedGrant, scopeId: claims.scopeId, sessionId, runner });
31
+ this.bindings.set(runId, {
32
+ userId: claims.userId,
33
+ grant: parsedGrant,
34
+ scopeId: claims.scopeId,
35
+ sessionId,
36
+ runner
37
+ });
31
38
  }
32
39
  catch {
33
40
  nodeLog('model.usage.invalid', { code: 'USAGE_GRANT_INVALID' });
@@ -36,6 +43,27 @@ export class NodeModelUsageService {
36
43
  release(runId) {
37
44
  this.bindings.delete(runId);
38
45
  }
46
+ completion(executionId, grant, sessionId, expectedUserId) {
47
+ this.bind(executionId, grant, sessionId, 'mar-agent');
48
+ if (expectedUserId !== undefined && this.bindings.get(executionId)?.userId !== expectedUserId)
49
+ this.release(executionId);
50
+ if (!this.bindings.has(executionId))
51
+ throw new Error('PROMPT_COMPLETION_USAGE_GRANT_REQUIRED');
52
+ return {
53
+ onModelUsage: (usage) => this.record(executionId, {
54
+ ...normalizeMarAgentUsage({
55
+ ...usage,
56
+ sessionId,
57
+ executionId,
58
+ purpose: 'other',
59
+ agentRole: 'root'
60
+ }),
61
+ purpose: 'prompt_completion'
62
+ }),
63
+ onModelUsageFailure: () => nodeLog('model.usage.invalid', { code: 'PROMPT_COMPLETION_USAGE_FAILED' }),
64
+ release: () => this.release(executionId)
65
+ };
66
+ }
39
67
  promoteSession(previousId, nextId) {
40
68
  for (const [runId, binding] of this.bindings)
41
69
  if (binding.sessionId === previousId)
@@ -199,6 +199,16 @@ export class NativeSessionWatchService {
199
199
  has(sessionId) {
200
200
  return this.watches.has(sessionId);
201
201
  }
202
+ observes(sessionHash, connectionId, workspaceId, sessionId) {
203
+ const ownerKey = `${sessionHash}:${connectionId}`;
204
+ const lease = this.leases.get(ownerKey);
205
+ return (lease !== undefined &&
206
+ lease.ready &&
207
+ lease.expiresAt > Date.now() &&
208
+ lease.sessionId === sessionId &&
209
+ lease.workspaceId === workspaceId &&
210
+ this.sessionWatchSlots.owns(ownerKey, lease.slotToken));
211
+ }
202
212
  isWatching(sessionId) {
203
213
  return this.has(sessionId);
204
214
  }
@@ -0,0 +1,66 @@
1
+ import { isAbsolute, win32 } from 'node:path';
2
+ /** Browser observations are hints, not authority to read paths or execute instructions. */
3
+ export async function projectPredictionAttention(events, read, signal, now = Date.now()) {
4
+ const result = [];
5
+ let remaining = 4096;
6
+ for (const event of events.slice(-20).reverse()) {
7
+ signal.throwIfAborted();
8
+ if (event.observedAt > now ||
9
+ now - event.observedAt >= 180_000 ||
10
+ isAbsolute(event.path) ||
11
+ win32.isAbsolute(event.path) ||
12
+ event.path.split(/[\\/]/u).includes('..') ||
13
+ event.path.includes('\0') ||
14
+ event.path.startsWith('mar-attachment:'))
15
+ continue;
16
+ const metadata = {
17
+ kind: event.kind,
18
+ path: event.path,
19
+ worktreePath: event.worktreePath,
20
+ source: event.source,
21
+ observedAt: event.observedAt
22
+ };
23
+ // Diff blocks without a verified content identity are metadata-only, never read in full.
24
+ if (event.kind === 'DIFF' || !event.revision || event.startLine === undefined) {
25
+ result.push(metadata);
26
+ if (result.length >= 8)
27
+ break;
28
+ continue;
29
+ }
30
+ try {
31
+ const value = (await read(event));
32
+ signal.throwIfAborted();
33
+ if (value.revision !== event.revision ||
34
+ value.encoding !== 'utf-8' ||
35
+ typeof value.content !== 'string')
36
+ continue;
37
+ const lines = value.content.split('\n');
38
+ // A bounded prefix can end mid-line. Never present that tail as a verified full line.
39
+ if (value.truncated)
40
+ lines.pop();
41
+ const endLine = Math.min(event.endLine, event.startLine + 79, lines.length);
42
+ if (event.startLine > endLine)
43
+ continue;
44
+ const text = lines
45
+ .slice(event.startLine - 1, endLine)
46
+ .join('\n')
47
+ .slice(0, Math.min(2048, remaining));
48
+ remaining -= text.length;
49
+ result.push({
50
+ ...metadata,
51
+ revision: value.revision,
52
+ startLine: event.startLine,
53
+ endLine,
54
+ text
55
+ });
56
+ }
57
+ catch (error) {
58
+ if (signal.aborted)
59
+ throw error;
60
+ // Missing, moved, changed, binary or inaccessible optional references are omitted.
61
+ }
62
+ if (result.length >= 8 || remaining <= 0)
63
+ break;
64
+ }
65
+ return JSON.stringify(result);
66
+ }