@anhlp/alp-runtime 0.0.0-beta.1 → 0.0.0-beta.2

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.
@@ -58,6 +58,18 @@ export declare function acpWhat(request: AcpPermission): string;
58
58
  */
59
59
  export declare function acpEnv(provider: Pick<AcpProvider, 'command' | 'env'>, base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
60
60
  /** Maps an ACP agent over stdio to the normalized runtime protocol. */
61
+ /** One turn-input part as ACP's: text, or a data-URL image as an ACP image block; anything else is dropped. */
62
+ export declare function acpContent(part: any): {
63
+ type: string;
64
+ text: any;
65
+ mimeType?: undefined;
66
+ data?: undefined;
67
+ } | {
68
+ type: string;
69
+ mimeType: string;
70
+ data: string;
71
+ text?: undefined;
72
+ } | null;
61
73
  export declare class AcpTransport {
62
74
  private readonly provider;
63
75
  private readonly cwd;
@@ -71,6 +83,8 @@ export declare class AcpTransport {
71
83
  private requestHandler?;
72
84
  private closed;
73
85
  private capabilities;
86
+ /** ACP agents say in initialize whether a prompt may hold images (promptCapabilities.image). */
87
+ takesImages(): boolean;
74
88
  private sessionId;
75
89
  private config;
76
90
  /** Instructions go with the first prompt of a new session; a loaded one has them already. */
@@ -95,6 +109,8 @@ export declare class AcpTransport {
95
109
  request(method: string, params: any): Promise<any>;
96
110
  /** Picks the session's model when ALP was given one and the agent offers a choice. */
97
111
  private chooseModel;
112
+ /** The tokens of the turn the prompt response says (ACP's unstable usage), before its end. */
113
+ private turnUsage;
98
114
  private endTurn;
99
115
  private closeMessage;
100
116
  private update;
@@ -9,6 +9,7 @@ import path from "node:path";
9
9
  import { createInterface } from "node:readline";
10
10
  import { commandDecision, unwrapShell } from "@anhlp/alp-core/permissions";
11
11
  import { launchCommand } from "@anhlp/alp-core/providers";
12
+ import { acpText, acpToolDetail } from "./tool-detail.js";
12
13
  var BRIDGE = `
13
14
  const net = require('node:net');
14
15
  const readline = require('node:readline');
@@ -92,6 +93,11 @@ function acpServers(servers, http) {
92
93
  return { name, command: server.command, args: server.args ?? [], env: Object.entries(server.env ?? {}).map(([key, value]) => ({ name: key, value: String(value) })) };
93
94
  });
94
95
  }
96
+ function acpContent(part) {
97
+ if (typeof part?.text === "string") return { type: "text", text: part.text };
98
+ const image = part?.type === "image" && typeof part.url === "string" ? /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(part.url) : null;
99
+ return image ? { type: "image", mimeType: image[1], data: image[2] } : null;
100
+ }
95
101
  var AcpTransport = class {
96
102
  constructor(provider, cwd, env) {
97
103
  this.provider = provider;
@@ -107,6 +113,10 @@ var AcpTransport = class {
107
113
  requestHandler;
108
114
  closed = false;
109
115
  capabilities = {};
116
+ /** ACP agents say in initialize whether a prompt may hold images (promptCapabilities.image). */
117
+ takesImages() {
118
+ return this.capabilities?.promptCapabilities?.image === true;
119
+ }
110
120
  sessionId = "";
111
121
  config;
112
122
  /** Instructions go with the first prompt of a new session; a loaded one has them already. */
@@ -191,7 +201,8 @@ var AcpTransport = class {
191
201
  const turnId = randomUUID();
192
202
  this.activeTurn = turnId;
193
203
  this.message = void 0;
194
- const prompt = (params.input ?? []).filter((part) => typeof part?.text === "string").map((part) => ({ type: "text", text: part.text }));
204
+ const prompt = (params.input ?? []).map(acpContent).filter(Boolean);
205
+ if (prompt.some((part) => part.type === "image") && !this.takesImages()) throw new Error(`${this.provider.label} takes no images`);
195
206
  if (!this.instructionsSent && this.config?.developerInstructions) {
196
207
  prompt.unshift({ type: "text", text: `Instructions from ALP, which runs you. Follow them for this whole session.
197
208
 
@@ -199,7 +210,10 @@ ${this.config.developerInstructions}` });
199
210
  this.instructionsSent = true;
200
211
  }
201
212
  this.prompting = this.call("session/prompt", { sessionId: this.sessionId, prompt }).then(
202
- (result) => this.endTurn(turnId, result?.stopReason),
213
+ (result) => {
214
+ this.turnUsage(turnId, result?.usage);
215
+ this.endTurn(turnId, result?.stopReason);
216
+ },
203
217
  (error) => this.endTurn(turnId, "error", error instanceof Error ? error.message : String(error))
204
218
  ).finally(() => {
205
219
  if (this.activeTurn === void 0 || this.activeTurn === turnId) this.prompting = void 0;
@@ -234,6 +248,12 @@ ${this.config.developerInstructions}` });
234
248
  if (!models.availableModels.some((model) => model?.modelId === wanted)) throw new Error(`${this.provider.label} has no model '${wanted}'`);
235
249
  await this.call("session/set_model", { sessionId: this.sessionId, modelId: wanted }, 3e4);
236
250
  }
251
+ /** The tokens of the turn the prompt response says (ACP's unstable usage), before its end. */
252
+ turnUsage(turnId, usage) {
253
+ if (this.closed || this.activeTurn !== turnId || !usage || typeof usage !== "object") return;
254
+ const last = { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, cachedInputTokens: usage.cachedReadTokens };
255
+ this.emit("thread/tokenUsage/updated", { threadId: this.sessionId, turnId, tokenUsage: { last } });
256
+ }
237
257
  endTurn(turnId, stopReason, error) {
238
258
  if (this.closed || this.activeTurn !== turnId) return;
239
259
  this.closeMessage();
@@ -264,22 +284,34 @@ ${this.config.developerInstructions}` });
264
284
  const id = String(update.toolCallId ?? "");
265
285
  if (!id) return;
266
286
  let call = this.toolCalls.get(id);
287
+ const first = !call;
288
+ if (call?.done) return;
267
289
  if (!call) {
268
290
  const request = acpPermission(update, this.tools.map((tool) => tool.name));
269
- call = { command: request.command ?? `${request.kind}: ${request.title}`, alp: request.alpTool };
291
+ call = { command: request.command ?? `${request.kind}: ${request.title}`, alp: request.alpTool, state: {} };
270
292
  this.toolCalls.set(id, call);
271
293
  this.closeMessage();
272
- if (!call.alp) this.emit("item/started", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: "inProgress", aggregatedOutput: "" } });
273
294
  }
274
- if ((update.status === "completed" || update.status === "failed") && !call.alp) {
295
+ for (const field of ["kind", "title", "rawInput", "rawOutput", "locations", "content"]) if (update[field] !== void 0 && update[field] !== null) call.state[field] = update[field];
296
+ if (call.alp) return;
297
+ const detail = acpToolDetail(call.state);
298
+ const typed = detail ? { tool: call.state.title || call.state.kind || "tool", detail } : {};
299
+ const done = update.status === "completed" || update.status === "failed";
300
+ if (first) this.emit("item/started", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: "inProgress", aggregatedOutput: "", ...typed } });
301
+ else if (!done && detail) this.emit("item/updated", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: "inProgress", aggregatedOutput: "", ...typed } });
302
+ if (done) {
303
+ call.state = {};
304
+ call.done = true;
275
305
  const output = (Array.isArray(update.content) ? update.content : []).map((part) => part?.type === "content" && part.content?.type === "text" ? part.content.text : part?.type === "diff" ? `diff ${part.path}` : "").filter(Boolean).join("\n");
276
- this.emit("item/completed", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: update.status, exitCode: update.status === "failed" ? 1 : 0, aggregatedOutput: output } });
306
+ const error = update.status === "failed" && detail ? { error: acpText(update.content) ?? (typeof update.rawOutput === "string" ? update.rawOutput : void 0) } : {};
307
+ this.emit("item/completed", { threadId: this.sessionId, item: { type: "commandExecution", id, command: call.command, cwd: this.cwd, status: update.status, exitCode: update.status === "failed" ? 1 : 0, aggregatedOutput: output, ...typed, ...error } });
277
308
  }
278
309
  return;
279
310
  }
280
311
  if (kind === "usage_update" && typeof update.used === "number") {
281
312
  const last = { totalTokens: update.used };
282
- this.emit("thread/tokenUsage/updated", { threadId: this.sessionId, turnId: this.activeTurn, tokenUsage: { last, total: last, ...typeof update.size === "number" ? { modelContextWindow: update.size } : {} } });
313
+ const cost = update.cost?.currency === "USD" && typeof update.cost.amount === "number" ? { totalCostUsd: update.cost.amount } : {};
314
+ this.emit("thread/tokenUsage/updated", { threadId: this.sessionId, turnId: this.activeTurn, tokenUsage: { last, total: last, ...typeof update.size === "number" ? { modelContextWindow: update.size } : {}, ...cost } });
283
315
  }
284
316
  }
285
317
  /** The agent asks before using a tool; ALP's runtime decides, and the answer picks one of the agent's options. */
@@ -471,6 +503,7 @@ ${this.config.developerInstructions}` });
471
503
  };
472
504
  export {
473
505
  AcpTransport,
506
+ acpContent,
474
507
  acpDecision,
475
508
  acpEnv,
476
509
  acpPermission,
@@ -33,6 +33,8 @@ export declare class AntigravityTransport {
33
33
  private readonly cwd;
34
34
  private readonly env;
35
35
  private readonly provider?;
36
+ /** Antigravity's prompt is one line of text: no images. */
37
+ takesImages(): boolean;
36
38
  private child?;
37
39
  private lines?;
38
40
  private exit;
@@ -8,6 +8,7 @@ import os from "node:os";
8
8
  import path from "node:path";
9
9
  import { createInterface } from "node:readline";
10
10
  import { openMcpTools } from "./mcp-tools.js";
11
+ import { antigravityToolDetail } from "./tool-detail.js";
11
12
  var BRIDGE = `// ALP Antigravity bridge v1
12
13
 
13
14
  const net = require('node:net');
@@ -53,6 +54,10 @@ var AntigravityTransport = class {
53
54
  this.env = env;
54
55
  this.provider = provider;
55
56
  }
57
+ /** Antigravity's prompt is one line of text: no images. */
58
+ takesImages() {
59
+ return false;
60
+ }
56
61
  child;
57
62
  lines;
58
63
  exit = Promise.resolve();
@@ -331,6 +336,8 @@ ${promptText}`;
331
336
  if (step.step_type === "tool") {
332
337
  const toolName = step.tool_name || "tool";
333
338
  const callId = `agy-tool-${this.activeTurn}-${step.step_index ?? ++this.messages}`;
339
+ const detail = antigravityToolDetail(step.tool_info ?? { name: toolName });
340
+ const typed = { tool: typeof step.tool_info?.name === "string" && step.tool_info.name ? step.tool_info.name : toolName, detail };
334
341
  if (step.state === "ACTIVE") {
335
342
  this.emit("item/started", {
336
343
  threadId: this.conversationId,
@@ -340,7 +347,8 @@ ${promptText}`;
340
347
  command: toolName,
341
348
  cwd: this.cwd,
342
349
  status: "inProgress",
343
- aggregatedOutput: ""
350
+ aggregatedOutput: "",
351
+ ...typed
344
352
  }
345
353
  });
346
354
  } else if (step.state === "DONE" || step.state === "ERROR") {
@@ -354,7 +362,9 @@ ${promptText}`;
354
362
  cwd: this.cwd,
355
363
  status: step.state === "DONE" ? "completed" : "failed",
356
364
  exitCode: step.state === "DONE" ? 0 : 1,
357
- aggregatedOutput: output
365
+ aggregatedOutput: output,
366
+ ...typed,
367
+ ...step.state === "ERROR" ? { error: step.tool_info?.error?.message } : {}
358
368
  }
359
369
  });
360
370
  }
@@ -24,6 +24,20 @@ type PermissionRules = {
24
24
  /** Whether Claude Code can sandbox Bash here: Seatbelt on macOS, bubblewrap and socat on Linux. ALP_CLAUDE_SANDBOX=0 turns it off, =1 claims it (tests). */
25
25
  export declare function claudeSandboxAvailable(): boolean;
26
26
  /** What ALP asks the user for a permission Claude would otherwise not have. */
27
+ /** One turn-input part as Claude's: text, or a data-URL image as a base64 image block. */
28
+ export declare function claudeContent(part: any): {
29
+ type: string;
30
+ source: {
31
+ type: string;
32
+ media_type: string;
33
+ data: string;
34
+ };
35
+ text?: undefined;
36
+ } | {
37
+ type: string;
38
+ text: any;
39
+ source?: undefined;
40
+ };
27
41
  export type PermissionRequest = {
28
42
  tool: string;
29
43
  input: Record<string, unknown>;
@@ -7,6 +7,7 @@ import { optionalRead, claudeUsage } from "./runtime-context.js";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { PIN_KINDS } from "./board.js";
10
+ import { claudeToolDetail, resultText } from "./tool-detail.js";
10
11
  import { CLOSE_REASONS, GATE_KINDS, TASK_STATUSES, TASK_TYPES } from "@anhlp/alp-core/tasks";
11
12
  import { commandDecision } from "@anhlp/alp-core/permissions";
12
13
  var CLAUDE_SDK = ["@anthropic-ai", "claude-agent-sdk"].join("/");
@@ -158,6 +159,11 @@ function claudeSandboxAvailable() {
158
159
  return onPath("bwrap") && onPath("socat");
159
160
  }
160
161
  var WRITERS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
162
+ function claudeContent(part) {
163
+ const image = part?.type === "image" && typeof part.url === "string" ? /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(part.url) : null;
164
+ if (part?.type === "image" && !image) throw new Error("Claude takes images as inline base64 PNG, JPEG, GIF or WebP only");
165
+ return image ? { type: "image", source: { type: "base64", media_type: image[1], data: image[2] } } : { type: "text", text: part.text };
166
+ }
161
167
  function suggestedRule(tool, input, suggestions) {
162
168
  const suggestion = (Array.isArray(suggestions) ? suggestions : []).find((entry) => entry?.type === "addRules" && entry.behavior === "allow" && entry.rules?.length);
163
169
  const rule = suggestion?.rules[0];
@@ -365,10 +371,7 @@ var ClaudeTransport = class {
365
371
  type: "user",
366
372
  message: {
367
373
  role: "user",
368
- content: params.input.map((part) => ({
369
- type: "text",
370
- text: part.text
371
- }))
374
+ content: params.input.map(claudeContent)
372
375
  },
373
376
  parent_tool_use_id: null,
374
377
  uuid: randomUUID(),
@@ -558,6 +561,8 @@ var ClaudeTransport = class {
558
561
  });
559
562
  }
560
563
  if (message.type === "result") {
564
+ const cost = message.total_cost_usd;
565
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) this.emit("thread/tokenUsage/updated", { threadId: this.threadId, turnId: this.activeTurn, tokenUsage: { totalCostUsd: cost } });
561
566
  const windows = Object.values(message.modelUsage ?? {}).map((entry) => entry?.contextWindow).filter((value) => typeof value === "number" && value > 0);
562
567
  if (windows.length && !this.compactAt) this.contextWindow = Math.max(...windows);
563
568
  }
@@ -573,7 +578,8 @@ var ClaudeTransport = class {
573
578
  });
574
579
  } else if (block.type === "tool_use") {
575
580
  const command = block.name === "Bash" ? String(block.input?.command ?? "") : `${block.name} ${JSON.stringify(block.input ?? {})}`;
576
- this.toolCalls.set(block.id, { name: block.name, command });
581
+ this.toolCalls.set(block.id, { name: block.name, command, input: block.input });
582
+ const detail = claudeToolDetail(block.name, block.input);
577
583
  this.emit("item/started", {
578
584
  threadId: this.threadId,
579
585
  item: {
@@ -582,7 +588,8 @@ var ClaudeTransport = class {
582
588
  command,
583
589
  cwd: this.cwd,
584
590
  status: "inProgress",
585
- aggregatedOutput: ""
591
+ aggregatedOutput: "",
592
+ ...detail ? { tool: block.name, detail } : {}
586
593
  }
587
594
  });
588
595
  }
@@ -591,11 +598,14 @@ var ClaudeTransport = class {
591
598
  }
592
599
  if (message.type === "user") {
593
600
  const content = Array.isArray(message.message.content) ? message.message.content : [];
594
- for (const block of content) {
595
- if (block.type !== "tool_result") continue;
601
+ const results = content.filter((block) => block.type === "tool_result");
602
+ const structured = results.length === 1 ? message.tool_use_result : void 0;
603
+ for (const block of results) {
596
604
  const call = this.toolCalls.get(block.tool_use_id);
597
605
  if (!call) continue;
598
606
  const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
607
+ const text = resultText(block.content);
608
+ const detail = claudeToolDetail(call.name, call.input, { text, structured, error: Boolean(block.is_error) });
599
609
  this.emit("item/completed", {
600
610
  threadId: this.threadId,
601
611
  item: {
@@ -605,7 +615,8 @@ var ClaudeTransport = class {
605
615
  cwd: this.cwd,
606
616
  status: block.is_error ? "failed" : "completed",
607
617
  exitCode: block.is_error ? 1 : 0,
608
- aggregatedOutput: output
618
+ aggregatedOutput: output,
619
+ ...detail ? { tool: call.name, detail, ...block.is_error ? { error: text } : {} } : {}
609
620
  }
610
621
  });
611
622
  this.toolCalls.delete(block.tool_use_id);
@@ -662,6 +673,7 @@ var ClaudeTransport = class {
662
673
  export {
663
674
  ClaudeTransport,
664
675
  SKILL_PLUGIN,
676
+ claudeContent,
665
677
  claudeLimited,
666
678
  claudePermissionMode,
667
679
  claudePermissions,
@@ -15,6 +15,8 @@ export type RuntimeTransport = {
15
15
  onFailure(listener: (error: unknown) => void): void;
16
16
  close(): Promise<void>;
17
17
  onRequest?: (listener: (method: string, params: any) => Promise<unknown>) => void;
18
+ /** Whether turn input may hold images (`{ type: 'image', url: 'data:…' }`); omitted, Codex and Claude do. */
19
+ takesImages?: () => boolean;
18
20
  };
19
21
  export type RuntimeOptions = {
20
22
  /** Native adapter overrides, isolated to this runtime; the legacy transport hook takes precedence. */
@@ -155,8 +157,12 @@ export type AlpRuntime = {
155
157
  };
156
158
  import type { SessionPreview } from '@anhlp/alp-protocol/session';
157
159
  export type { SessionPreview } from '@anhlp/alp-protocol/session';
158
- /** The team a viewer starts a root in when neither it nor the project's settings choose one, as Paseo does. */
159
- export declare const DEFAULT_TEAM = "pho";
160
+ /**
161
+ * The team `alp init` names in a new project's settings (core INIT_TEAM). Kept for compatibility;
162
+ * it is not the default of a session: preview and create both resolve that through core
163
+ * `defaultTeam` (settings without workflow.mode run `custom`).
164
+ */
165
+ export { INIT_TEAM as DEFAULT_TEAM } from '@anhlp/alp-core/workflow';
160
166
  import type { RecoveryOutcome, Pause, PauseState } from '@anhlp/alp-protocol/session';
161
167
  export type { RecoveryOutcome, Pause, PauseState } from '@anhlp/alp-protocol/session';
162
168
  import type { RecallAnswer } from '@anhlp/alp-protocol/session';
package/dist/contracts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // packages/runtime/src/contracts.ts
2
- var DEFAULT_TEAM = "pho";
2
+ import { INIT_TEAM } from "@anhlp/alp-core/workflow";
3
3
  export {
4
- DEFAULT_TEAM
4
+ INIT_TEAM as DEFAULT_TEAM
5
5
  };
package/dist/index.d.ts CHANGED
@@ -22,3 +22,5 @@ export type { Words } from './language.js';
22
22
  export type { Pin, PinKind } from './board.js';
23
23
  export { ProviderRegistry, createProviderRegistry } from './providers.js';
24
24
  export type { ProviderContext, ProviderFactory, ProviderFactories } from './providers.js';
25
+ export { acpToolDetail, antigravityToolDetail, boundDetail, claudeToolDetail, clipText, codexToolCall, stripReadGutter, toolError, DETAIL_FILE_PATHS, DETAIL_TEXT_CHARS, DETAIL_WEB_RESULTS, ERROR_CHARS, TOOL_FAILED } from './tool-detail.js';
26
+ export type { AcpCall, ClaudeResult, CodexToolCall, ToolCallDetail } from './tool-detail.js';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { OWN_START, processStartedAt, sameProcessAlive } from "./process-info.js
13
13
  import { PIN_KINDS, renderBoard, renderPin } from "./board.js";
14
14
  import { CHOICES, isVietnamese, languageInstruction, words } from "./language.js";
15
15
  import { ProviderRegistry, createProviderRegistry } from "./providers.js";
16
+ import { acpToolDetail, antigravityToolDetail, boundDetail, claudeToolDetail, clipText, codexToolCall, stripReadGutter, toolError, DETAIL_FILE_PATHS, DETAIL_TEXT_CHARS, DETAIL_WEB_RESULTS, ERROR_CHARS, TOOL_FAILED } from "./tool-detail.js";
16
17
  export {
17
18
  AcpTransport,
18
19
  AntigravityTransport,
@@ -22,6 +23,10 @@ export {
22
23
  DEFAULT_CLAUDE_MODEL,
23
24
  DEFAULT_MODEL,
24
25
  DEFAULT_TEAM,
26
+ DETAIL_FILE_PATHS,
27
+ DETAIL_TEXT_CHARS,
28
+ DETAIL_WEB_RESULTS,
29
+ ERROR_CHARS,
25
30
  InstructionsAdapter,
26
31
  MAIL_BATCH_CHARS,
27
32
  MAIL_BODY_CHARS,
@@ -29,13 +34,20 @@ export {
29
34
  PIN_KINDS,
30
35
  ProviderRegistry,
31
36
  SKILL_PLUGIN,
37
+ TOOL_FAILED,
32
38
  USER,
33
39
  acpDecision,
34
40
  acpEnv,
35
41
  acpPermission,
42
+ acpToolDetail,
43
+ antigravityToolDetail,
44
+ boundDetail,
36
45
  claudePermissions,
37
46
  claudeSandboxAvailable,
47
+ claudeToolDetail,
38
48
  toolShapes as claudeToolShapes,
49
+ clipText,
50
+ codexToolCall,
39
51
  createAlpRuntime,
40
52
  createCopy,
41
53
  createProviderRegistry,
@@ -56,9 +68,11 @@ export {
56
68
  sameProcessAlive,
57
69
  skillDescription,
58
70
  skillPlugin,
71
+ stripReadGutter,
59
72
  takeBatch,
60
73
  thinkingOptions,
61
74
  thinkingOptionsFor,
75
+ toolError,
62
76
  usageAllows,
63
77
  words
64
78
  };
package/dist/resolve.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // packages/runtime/src/resolve.ts
2
2
  import path from "node:path";
3
3
  import { access, readFile, stat } from "node:fs/promises";
4
- import { resolveWorkflow } from "@anhlp/alp-core/workflow";
4
+ import { defaultTeam, resolveWorkflow } from "@anhlp/alp-core/workflow";
5
5
  import { initProject } from "@anhlp/alp-core/init";
6
6
  import { profileModel, agentProfiles, selectedProfile } from "@anhlp/alp-core/profiles";
7
7
  import { jsonObject, resolveAgent } from "@anhlp/alp-core/resolver";
@@ -68,13 +68,13 @@ async function resolveSession(spec, options = {}) {
68
68
  if (scaffoldMissing) await initProject(spec.cwd, { ...starter, library: options.library });
69
69
  else if (!spec.restore) {
70
70
  const settings = await jsonObject(settingsFile, "INVALID_SETTINGS");
71
- await initProject(spec.cwd, { ...starter, library: options.library, onlyManagedProfiles: true, teamIds: [teamOf(spec.workflow ?? settings.workflow?.mode ?? "custom")] });
71
+ await initProject(spec.cwd, { ...starter, library: options.library, onlyManagedProfiles: true, teamIds: [spec.workflow === void 0 ? defaultTeam(settings) : teamOf(spec.workflow)] });
72
72
  }
73
73
  }
74
74
  if (options.library) await ensureLibrary(options.library, options.templates ? { templates: options.templates } : {});
75
75
  const restored = spec.restore;
76
76
  if (restored && spec.agent !== void 0 && spec.agent !== restored.agent) throw new Error("Cannot resume a thread as a different ALP agent");
77
- const { team, ...workflow } = await resolveWorkflow(spec.cwd, spec.workflow, restored?.workflow, { library: options.library, templates: options.templates });
77
+ const { team, ...workflow } = await resolveWorkflow(spec.cwd, spec.workflow, restored?.workflow, { library: options.library, templates: options.templates, scaffold: options.initialize === false });
78
78
  const agent = await resolveAgent(spec.cwd, { agent: spec.agent ?? restored?.agent, library: options.library, templates: options.templates });
79
79
  const oracleModels = team?.oracleModels ?? ORACLE_MODELS;
80
80
  if (agent.name === "oracle" && !restored && !oracleModels.includes(spec.model ?? "")) throw new Error(`Oracle runs on ${oracleModels.join(" or ")}; choose one`);
package/dist/runtime.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import { errorData, plainObject, toolResult } from "./tool-results.js";
4
4
  import { createLogger } from "@anhlp/alp-core/log";
5
+ import { PROMPT_IMAGES, base64Bytes } from "@anhlp/alp-protocol";
5
6
  import { createBoardDomain } from "./board-domain.js";
6
7
  import { createWorktreeDomain } from "./worktree-domain.js";
7
8
  import { DEFAULT_TEAM } from "./contracts.js";
8
- import { DEFAULT_TEAM as DEFAULT_TEAM2 } from "./contracts.js";
9
9
  import { verdictLine, parseHandoff } from "./handoff.js";
10
10
  import { TASK_ACTIONS, TASK_PAST, TASK_FIELDS, mainOf, roleOf, taskActions, taskBrief } from "./tools.js";
11
11
  import { nativeEnvironment, createTransport } from "./providers.js";
@@ -36,9 +36,10 @@ import { OWN_START, sameProcessAlive } from "./process-info.js";
36
36
  import { createRecallBook, recallPrompt, RECALL_KEEP_MS, RECALL_QUESTION_CHARS, RECALL_TIMEOUT_MS } from "./recall.js";
37
37
  import { branchExists, checkoutFingerprint, checkoutKey, createCopy, createWorktree, reattachWorktree, removeCopy, removeWorktree } from "./workspace.js";
38
38
  import { claudeSandboxAvailable, SKILL_PLUGIN } from "./claude-transport.js";
39
+ import { boundDetail, codexToolCall, toolError } from "./tool-detail.js";
39
40
  import { ALP_REPO, gh, projectRepo } from "./github.js";
40
41
  import { optionalRead, usageAllows, COMPACT_SHARE } from "./runtime-context.js";
41
- import { providerUsage } from "./usage.js";
42
+ import { providerUsage, sessionUsage } from "./usage.js";
42
43
  import { parse as toml } from "smol-toml";
43
44
  import { findFormula, formulaDirs, listFormulas, pourFormula } from "@anhlp/alp-core/formulas";
44
45
  import { addAllowRule, capMode, commandDecision, profileFor, unwrapShell } from "@anhlp/alp-core/permissions";
@@ -119,6 +120,9 @@ function createAlpRuntime(options = {}) {
119
120
  });
120
121
  return run;
121
122
  }
123
+ function takesImages(session) {
124
+ return session.runtime.takesImages ? session.runtime.takesImages() : session.runtimeKind === "codex" || session.runtimeKind === "claude";
125
+ }
122
126
  function snapshot(sessionId, session) {
123
127
  return {
124
128
  id: sessionId,
@@ -137,6 +141,8 @@ function createAlpRuntime(options = {}) {
137
141
  ...session.active ? { activeTurnId: session.active } : {},
138
142
  ...session.parked ? { parked: session.parked.reason } : {},
139
143
  ...session.instructionsSha ? { instructionsSha: session.instructionsSha } : {},
144
+ ...takesImages(session) ? { promptImages: true } : {},
145
+ ...session.usage ? { usage: session.usage } : {},
140
146
  busy: !!(session.active || session.pending || session.children.size || session.assignments.size || hasActiveMail(session) || reviewing(session))
141
147
  };
142
148
  }
@@ -186,7 +192,7 @@ function createAlpRuntime(options = {}) {
186
192
  }
187
193
  session.settle?.(state, error);
188
194
  }
189
- function nativeItem(sessionId, session, item) {
195
+ function nativeItem(sessionId, session, item, done = false) {
190
196
  if (item.type === "agentMessage") {
191
197
  if (session.userWaiting !== void 0 && String(item.text ?? "").trim()) {
192
198
  if (session.supervisor) session.journal.push(stamp(`${session.mapping.agent.name} answered the user ${span(Date.now() - session.userWaiting)} after their message of ${clock(session.userWaiting)}`));
@@ -198,7 +204,8 @@ function createAlpRuntime(options = {}) {
198
204
  }
199
205
  if (item.type === "userMessage") {
200
206
  const text2 = (item.content ?? []).filter((c) => c.type === "text").map((c) => c.text).join("\n");
201
- emit(sessionId, { type: "item", item: { kind: "user_message", id: item.id, text: text2 } });
207
+ const images = (item.content ?? []).filter((c) => c.type === "image" || c.type === "localImage").length;
208
+ emit(sessionId, { type: "item", item: { kind: "user_message", id: item.id, text: text2, ...images ? { images } : {} } });
202
209
  return;
203
210
  }
204
211
  if (item.type === "dynamicToolCall") {
@@ -229,24 +236,33 @@ function createAlpRuntime(options = {}) {
229
236
  if (session.supervisor && status !== "running") {
230
237
  session.journal.push(stamp(`${session.mapping.agent.name} shell ${status}${item.exitCode != null ? ` (exit ${item.exitCode})` : ""}: ${clip(String(item.command ?? ""))}`));
231
238
  }
239
+ const typed = item.detail && typeof item.detail === "object" && item.detail.type !== "shell" ? boundDetail(item.detail) : void 0;
232
240
  emit(sessionId, {
233
241
  type: "item",
234
242
  item: {
235
243
  kind: "tool_call",
236
244
  id: item.id,
237
245
  callId: item.id,
238
- name: "shell",
246
+ name: typed ? String(item.tool || "tool") : "shell",
239
247
  status,
240
- ...status === "failed" ? { error: "Command failed" } : {},
241
- detail: {
248
+ ...status === "failed" ? { error: typed ? toolError(item.error) : "Command failed" } : {},
249
+ detail: typed ?? (item.detail?.type === "shell" ? item.detail : {
242
250
  type: "shell",
243
251
  command: item.command,
244
252
  cwd: item.cwd,
245
253
  output: item.aggregatedOutput ?? "",
246
254
  exitCode: item.exitCode
247
- }
255
+ })
248
256
  }
249
257
  });
258
+ return;
259
+ }
260
+ const call = codexToolCall(item, done);
261
+ if (call) {
262
+ emit(sessionId, {
263
+ type: "item",
264
+ item: { kind: "tool_call", id: item.id, callId: item.id, name: call.name, status: call.status, ...call.error ? { error: call.error } : {}, detail: call.detail }
265
+ });
250
266
  }
251
267
  }
252
268
  function notification(sessionId, session, method, params) {
@@ -270,17 +286,26 @@ function createAlpRuntime(options = {}) {
270
286
  if (method === "item/completed" || method === "item/started") {
271
287
  if (method === "item/completed") session.progressAt = Date.now();
272
288
  if (params.item?.type !== "userMessage") {
273
- nativeItem(sessionId, session, params.item);
289
+ nativeItem(sessionId, session, params.item, method === "item/completed");
274
290
  }
275
291
  if (method === "item/started" && params.item?.type === "commandExecution") watchCopy(sessionId, session, params.item);
276
292
  if (method === "item/started" && params.item) skillUse(sessionId, session, params.item);
277
293
  return;
278
294
  }
295
+ if (method === "item/updated") {
296
+ if (params.item?.type === "commandExecution" && params.item.status === "inProgress") nativeItem(sessionId, session, params.item);
297
+ return;
298
+ }
279
299
  if (method === "account/rateLimits/updated") {
280
300
  usageReport(session, params);
281
301
  return;
282
302
  }
283
303
  if (method === "thread/tokenUsage/updated") {
304
+ const usage2 = sessionUsage(session.usage, params?.tokenUsage);
305
+ if (usage2) {
306
+ session.usage = usage2;
307
+ emit(sessionId, { type: "usage", usage: usage2 });
308
+ }
284
309
  contextReport(sessionId, session, params?.tokenUsage);
285
310
  return;
286
311
  }
@@ -2656,12 +2681,20 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
2656
2681
  )) {
2657
2682
  return;
2658
2683
  }
2659
- if (prompt.content.some(
2660
- (content) => content.type !== "text"
2661
- )) {
2662
- throw new Error(
2663
- "Only text prompts are supported"
2664
- );
2684
+ if (prompt.content.some((content) => content.type !== "text" && content.type !== "image")) {
2685
+ throw new Error("Only text and image prompts are supported");
2686
+ }
2687
+ const images = prompt.content.filter((content) => content.type === "image");
2688
+ if (images.length) {
2689
+ if (!takesImages(session)) throw new Error(`${session.runtimeKind === "acp" ? "This ACP agent" : session.runtimeKind[0].toUpperCase() + session.runtimeKind.slice(1)} takes no images; send the message as text`);
2690
+ if (images.length > PROMPT_IMAGES.maxCount) throw new Error(`A prompt takes at most ${PROMPT_IMAGES.maxCount} images`);
2691
+ if (images.reduce((sum, image) => sum + (typeof image.data === "string" ? base64Bytes(image.data) : 0), 0) > PROMPT_IMAGES.maxTotalBytes) throw new Error(`The images of a prompt are at most ${PROMPT_IMAGES.maxTotalBytes} bytes together`);
2692
+ for (const image of images) {
2693
+ const keys = Object.keys(image).filter((key) => !["type", "mimeType", "data", "name"].includes(key));
2694
+ if (keys.length || !PROMPT_IMAGES.mimeTypes.includes(image.mimeType) || typeof image.data !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || base64Bytes(image.data) > PROMPT_IMAGES.maxBytes) {
2695
+ throw new Error(`An image must be inline base64 ${PROMPT_IMAGES.mimeTypes.join(", ")} of at most ${PROMPT_IMAGES.maxBytes} bytes`);
2696
+ }
2697
+ }
2665
2698
  }
2666
2699
  if (prompt.delivery === "steer" && !session.active) {
2667
2700
  throw new Error(
@@ -2673,9 +2706,7 @@ Write scope: change files only under ${session.spec.writeRoot}. Your working dir
2673
2706
  "Turn already active; use steering or interrupt first"
2674
2707
  );
2675
2708
  }
2676
- const text2 = prompt.content.map(
2677
- (content) => content.text ?? ""
2678
- ).join("\n");
2709
+ const text2 = prompt.content.filter((content) => content.type === "text").map((content) => content.text ?? "").join("\n");
2679
2710
  const mail = wake ?? (prompt.delivery === "steer" ? [] : takeBatch(session.mail, () => true));
2680
2711
  for (const event of mail) event.deliveredTurn = STARTING;
2681
2712
  session.pending = true;
@@ -2712,6 +2743,7 @@ ${interrupted}`), text_elements: [] }] : [],
2712
2743
  text: origin === "user" ? text2 : promptSafe(text2),
2713
2744
  text_elements: []
2714
2745
  },
2746
+ ...images.map((image) => ({ type: "image", url: `data:${image.mimeType};base64,${image.data}` })),
2715
2747
  ...mail.length && !wake ? [{ type: "text", text: promptSafe(renderMail(mail, session.parentAgent)), text_elements: [] }] : []
2716
2748
  ];
2717
2749
  const result = prompt.delivery === "steer" ? await session.runtime.request(
@@ -2744,7 +2776,8 @@ ${interrupted}`), text_elements: [] }] : [],
2744
2776
  kind: "user_message",
2745
2777
  id: `user:${prompt.clientMessageId}`,
2746
2778
  clientMessageId: prompt.clientMessageId,
2747
- text: text2
2779
+ text: text2,
2780
+ ...images.length ? { images: images.length } : {}
2748
2781
  }
2749
2782
  });
2750
2783
  if (origin === "user" && !session.parent) session.userWaiting ??= Date.now();
@@ -2893,10 +2926,7 @@ ${interrupted}`), text_elements: [] }] : [],
2893
2926
  },
2894
2927
  async preview(spec) {
2895
2928
  const resolveOptions = { templates: options.templates, library: options.libraryDir, language: options.language };
2896
- const settings = JSON.parse(await readFile(path.join(spec.cwd, ".alp", "settings.json"), "utf8").catch(() => "{}"));
2897
- const named = typeof settings?.workflow?.mode === "string" && settings.workflow.mode !== "custom" ? settings.workflow.mode : void 0;
2898
- const workflow = spec.workflow ?? named ?? DEFAULT_TEAM;
2899
- const mapping = await resolveSession({ ...spec, workflow }, { ...resolveOptions, initialize: false });
2929
+ const mapping = await resolveSession(spec, { ...resolveOptions, initialize: false });
2900
2930
  const teams = (await listTeams(spec.cwd, { library: options.libraryDir, templates: options.templates })).filter((team) => !team.error).map((team) => ({ id: team.id, label: team.label ?? team.id, ...team.description ? { description: team.description } : {} }));
2901
2931
  return {
2902
2932
  teams,
@@ -2974,6 +3004,6 @@ ${interrupted}`), text_elements: [] }] : [],
2974
3004
  };
2975
3005
  }
2976
3006
  export {
2977
- DEFAULT_TEAM2 as DEFAULT_TEAM,
3007
+ DEFAULT_TEAM,
2978
3008
  createAlpRuntime
2979
3009
  };