@stage-labs/metro 0.1.0-beta.312 → 0.1.0-beta.313

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.312",
3
+ "version": "0.1.0-beta.313",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,7 +43,7 @@ import { notReady, resolveRoute, routeLabel, type Connection, type ModelSetup, t
43
43
  import { codexMessages, type CodexDeps } from './codex.js';
44
44
  import { countTokens } from './subscription.js';
45
45
  import { OPENROUTER_BASE } from './openrouter.js';
46
- import { refusedWithPage, ROUTEWAY_BASE, routewayHeaders } from './routeway.js';
46
+ import { refusedWithPage, ROUTEWAY_BASE, routewayHeaders, routewayTools } from './routeway.js';
47
47
  import { isRecord } from '@metro-labs/core/is-record';
48
48
  import { forgetServed, noteServed } from './served.js';
49
49
  import { forgetUsage, noteUsageHeaders, UsageScanner } from './usage.js';
@@ -189,7 +189,9 @@ async function toRouteway(req: IncomingMessage, res: ServerResponse, body: Shape
189
189
  const conn = route.connection;
190
190
  const watch = watchUpstream(res);
191
191
  const send = sender(`${deps.routewayBase ?? ROUTEWAY_BASE}${upstreamPath(req)}`, watch);
192
- const upstream = await shapedAttempt(send, routewayHeaders(req, conn.apiKey), { ...body, bind: false }, route);
192
+ const sent = routewayTools(body.sent);
193
+ const fitted: Shaped = { raw: sent === body.sent ? body.raw : Buffer.from(JSON.stringify(sent)), sent, shaped: routewayTools(body.shaped), bind: false };
194
+ const upstream = await shapedAttempt(send, routewayHeaders(req, conn.apiKey), fitted, route);
193
195
  noteRefusal(conn.label, route.model, upstream);
194
196
  await refusedWithPage(upstream, watch);
195
197
  await pipeResponse(upstream, res, watch, { keepalive: true, ownCredential: true, scanner: new UsageScanner(conn.id) });
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
+ import { isRecord } from '@metro-labs/core/is-record';
2
3
  import { anthropicHeaders, GatewayError, type Watch } from './forward.js';
3
4
 
4
5
  export const ROUTEWAY_BASE = 'https://api.routeway.ai';
@@ -17,3 +18,15 @@ export async function refusedWithPage(upstream: Response, watch: Watch): Promise
17
18
  watch.stop();
18
19
  throw new GatewayError(502, 'api_error', `metro gateway: Routeway answered with a web page instead of its API (${String(upstream.status)}); try again in a moment`);
19
20
  }
21
+
22
+ function withProperties(tool: unknown): unknown {
23
+ if (!isRecord(tool) || !isRecord(tool.input_schema) || Object.hasOwn(tool.input_schema, 'properties')) return tool;
24
+ return { ...tool, input_schema: { ...tool.input_schema, properties: {} } };
25
+ }
26
+
27
+ export function routewayTools(body: Record<string, unknown>): Record<string, unknown> {
28
+ const listed: unknown = body.tools;
29
+ if (!Array.isArray(listed)) return body;
30
+ const tools: unknown[] = listed.map(withProperties);
31
+ return tools.some((tool, at) => tool !== listed[at]) ? { ...body, tools } : body;
32
+ }
@@ -10,7 +10,7 @@ export const CALL_TIMEOUT_MS = 120_000;
10
10
  const LIST_TIMEOUT_MS = 15_000;
11
11
  const REFRESH_MS = 5 * 60_000;
12
12
 
13
- export const NOT_SET_UP = 'Connectors are not set up on this agent: enroll it in its organization on the Server page of metro.box, then try again.';
13
+ export const NOT_SET_UP = 'Connectors are not set up on this agent yet: it links to its organization once an admin of the organization opens its page on metro.box. Try again after that.';
14
14
  export const UNAVAILABLE = 'mcp.metro.box is unavailable, try again.';
15
15
 
16
16
  export interface ConnectorResult {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.312"
2
+ "version": "0.1.0-beta.313"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.312",
2
+ "version": "0.1.0-beta.313",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -7,7 +7,7 @@ import { runnerFailureSummary, type RunnerActivity, type RunnerDiagnostic, type
7
7
  import { writeAtomic } from '@metro-labs/core/secure-fs';
8
8
  import { ActivityTasks, activityName } from './activity-tasks.js';
9
9
  import type { ApprovalAsk } from './approvals.js';
10
- import type { SavedTask } from './task-state.js';
10
+ import { sdkTask, type SavedTask } from './task-state.js';
11
11
  import { sessionFailure } from './errors.js';
12
12
  export { failureSummary } from './errors.js';
13
13
 
@@ -94,10 +94,11 @@ export class Activity {
94
94
  }
95
95
 
96
96
  recover(tasks: SavedTask[]): void {
97
- this.tasks.restore(tasks.map((task) => ({
98
- id: task.id, kind: null, agent: null, status: task.state === 'interrupted' ? 'unknown' : task.state,
97
+ const workers = tasks.filter(sdkTask);
98
+ this.tasks.restore(workers.map((task) => ({
99
+ id: task.id, kind: null, agent: null, status: task.state === 'interrupted' ? 'unknown' : task.state, ...(task.description === undefined ? {} : { description: task.description }),
99
100
  background: true, startedAt: 0, updatedAt: task.updatedAt, endedAt: null, lastTool: null, toolUses: 0, durationMs: 0,
100
- })), tasks.flatMap((task) => task.toolUseId === null ? [] : [[task.toolUseId, task.id]]));
101
+ })), workers.flatMap((task) => task.toolUseId === null ? [] : [[task.toolUseId, task.id]]));
101
102
  for (const task of tasks) {
102
103
  if (task.notice === null) continue;
103
104
  const code = task.notice.reason === 'worker_restart' ? 'interrupted' : task.notice.reason;
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
+ import { runnerTaskDescription } from '@metro-labs/core/runner-activity';
2
3
  import { activityName } from './activity-tasks.js';
3
4
  import type { Unanswered } from './inbox.js';
4
5
  import { pendingNotice, TASK_LIMIT, type SavedTask, type TaskReason } from './task-state.js';
@@ -67,9 +68,11 @@ export class TaskLedger {
67
68
  changed(handled?: ReadonlySet<string>): void { this.save(this.snapshot(), handled); }
68
69
 
69
70
  private taskEvent(id: string, m: Record<string, unknown>, event: string | null, now: number): void {
70
- if (m.subtype === 'task_started') this.start(id, m.tool_use_id, event, now);
71
+ const known = this.rows.get(id);
72
+ if (m.subtype === 'task_started') this.start(id, m.tool_use_id, event, now, m.description);
71
73
  else if (m.subtype === 'task_notification') this.end(id, m, event, now);
72
- else if (m.subtype === 'task_updated' && isRecord(m.patch) && this.rows.has(id)) {
74
+ else if (m.subtype === 'task_updated' && isRecord(m.patch) && known !== undefined) {
75
+ if (this.describe(known, m.patch.description)) this.save(this.snapshot());
73
76
  this.end(id, { status: m.patch.status === 'killed' ? 'stopped' : m.patch.status, summary: m.patch.error }, event, now);
74
77
  } else if (m.subtype === 'task_progress') this.progress(id, m.tool_use_id, now);
75
78
  }
@@ -114,10 +117,11 @@ export class TaskLedger {
114
117
  return task;
115
118
  }
116
119
 
117
- private start(id: string, rawTool: unknown, event: string | null, now: number): void {
120
+ private start(id: string, rawTool: unknown, event: string | null, now: number, description?: unknown): void {
118
121
  const task = this.get(id, now);
119
122
  if (task.state !== 'running') task.attempt += 1;
120
123
  task.state = 'running';
124
+ this.describe(task, description);
121
125
  if (event !== null) task.events = [...task.events, event].slice(-16);
122
126
  task.updatedAt = now;
123
127
  task.toolUseId = activityName(rawTool) ?? task.toolUseId;
@@ -165,13 +169,24 @@ export class TaskLedger {
165
169
 
166
170
  private background(m: Record<string, unknown>, now: number): void {
167
171
  if (!Array.isArray(m.tasks)) return;
172
+ let described = false;
168
173
  for (const row of m.tasks) {
169
174
  if (!isRecord(row)) continue;
170
175
  const id = activityName(row.task_id);
171
176
  if (id === null) continue;
177
+ const known = this.rows.get(id);
172
178
  if (row.ambient === true) this.hide(id);
173
- else if (!this.rows.has(id)) this.start(id, undefined, activityName(m.uuid), now);
179
+ else if (known === undefined) this.start(id, undefined, activityName(m.uuid), now, row.description);
180
+ else described = this.describe(known, row.description) || described;
174
181
  }
182
+ if (described) this.save(this.snapshot());
183
+ }
184
+
185
+ private describe(task: SavedTask, raw: unknown): boolean {
186
+ const description = runnerTaskDescription(raw);
187
+ if (description === null || description === task.description) return false;
188
+ task.description = description;
189
+ return true;
175
190
  }
176
191
 
177
192
  private content(m: Record<string, unknown>, now: number): void {
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
+ import { runnerTaskDescription } from '@metro-labs/core/runner-activity';
3
4
  import { activityName } from './activity-tasks.js';
4
5
  import type { Uuid } from './inbox.js';
5
6
 
@@ -26,6 +27,7 @@ export interface SavedTask {
26
27
  attempt: number;
27
28
  handledAttempt?: number;
28
29
  state: (typeof STATES)[number];
30
+ description?: string;
29
31
  events: string[];
30
32
  updatedAt: number;
31
33
  notice: TaskNotice | null;
@@ -35,6 +37,7 @@ const integer = (raw: unknown): raw is number => typeof raw === 'number' && Numb
35
37
  const name = (raw: unknown): raw is string | null => raw === null || activityName(raw) !== null;
36
38
  const reason = (raw: unknown): raw is TaskReason => REASONS.some((value) => value === raw);
37
39
  const optionalInteger = (raw: unknown): boolean => raw === undefined || integer(raw);
40
+ const description = (raw: unknown): boolean => raw === undefined || (typeof raw === 'string' && runnerTaskDescription(raw) === raw);
38
41
  const optionalUuid = (raw: unknown): boolean => raw === undefined || (typeof raw === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(raw));
39
42
 
40
43
  function notice(raw: unknown): raw is TaskNotice | null {
@@ -45,14 +48,17 @@ function notice(raw: unknown): raw is TaskNotice | null {
45
48
  }
46
49
 
47
50
  const events = (raw: unknown): boolean => Array.isArray(raw) && raw.length <= 16 && raw.every((event) => activityName(event) !== null);
51
+ const optionalFields = (raw: Record<string, unknown>): boolean => optionalInteger(raw.handledAttempt) && description(raw.description);
48
52
 
49
53
  function task(raw: unknown): raw is SavedTask {
50
54
  if (!isRecord(raw) || activityName(raw.id) === null) return false;
51
55
  if (![raw.toolUseId, raw.owner, raw.parentToolUseId ?? null].every(name)) return false;
52
- if (!events(raw.events) || !optionalInteger(raw.handledAttempt)) return false;
56
+ if (!events(raw.events) || !optionalFields(raw)) return false;
53
57
  return integer(raw.attempt) && integer(raw.updatedAt) && STATES.some((state) => state === raw.state) && notice(raw.notice);
54
58
  }
55
59
 
60
+ export const sdkTask = (task: SavedTask): boolean => !/^(?:input|turn):/.test(task.id);
61
+
56
62
  export function savedTasks(raw: unknown): SavedTask[] {
57
63
  if (raw === undefined) return [];
58
64
  if (!Array.isArray(raw) || raw.length > TASK_LIMIT || !raw.every(task) || new Set(raw.map((row) => row.id)).size !== raw.length)