@fastagent-sh/fastagent 0.22.1 → 0.24.0

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.
Files changed (128) hide show
  1. package/dist/agent.d.ts +10 -0
  2. package/dist/channels/agentcore-protocol.d.ts +2 -1
  3. package/dist/channels/agentcore-service.js +10 -18
  4. package/dist/channels/agentcore.js +6 -3
  5. package/dist/channels/control.js +23 -0
  6. package/dist/channels/kit/preview-kit.d.ts +6 -1
  7. package/dist/channels/kit/preview-kit.js +21 -4
  8. package/dist/channels/slack/config-api.d.ts +2 -2
  9. package/dist/channels/slack/config-api.js +2 -2
  10. package/dist/channels/slack/onboard.js +2 -2
  11. package/dist/channels/slack/preview.js +2 -1
  12. package/dist/channels/slack/register-webhook.js +2 -2
  13. package/dist/channels/slack/slack-api.js +4 -4
  14. package/dist/cli/add-feishu.js +4 -2
  15. package/dist/cli/auth-view.d.ts +15 -6
  16. package/dist/cli/auth-view.js +24 -8
  17. package/dist/cli/box-login.d.ts +33 -0
  18. package/dist/cli/box-login.js +162 -0
  19. package/dist/cli/commands/deploy/agentcore.js +48 -11
  20. package/dist/cli/commands/deploy/docker.js +24 -13
  21. package/dist/cli/commands/deploy/fly.js +30 -7
  22. package/dist/cli/commands/deploy/railway.js +21 -7
  23. package/dist/cli/commands/deploy/shared.d.ts +18 -17
  24. package/dist/cli/commands/deploy/shared.js +20 -21
  25. package/dist/cli/commands/destroy.js +2 -2
  26. package/dist/cli/commands/info.js +15 -12
  27. package/dist/cli/commands/invoke.js +2 -2
  28. package/dist/cli/commands/login.d.ts +7 -1
  29. package/dist/cli/commands/login.js +123 -10
  30. package/dist/cli/commands/logs.js +2 -2
  31. package/dist/cli/commands/models.d.ts +4 -1
  32. package/dist/cli/commands/models.js +37 -4
  33. package/dist/cli/commands/routine-run.js +2 -2
  34. package/dist/cli/commands/start.d.ts +0 -5
  35. package/dist/cli/commands/start.js +3 -13
  36. package/dist/cli/invoke-stream.js +14 -0
  37. package/dist/cli/kernel.d.ts +2 -0
  38. package/dist/cli/kernel.js +1 -1
  39. package/dist/cli/login-relay.d.ts +38 -0
  40. package/dist/cli/login-relay.js +121 -0
  41. package/dist/cli/program.js +46 -8
  42. package/dist/cli/shared.d.ts +6 -5
  43. package/dist/cli/shared.js +29 -36
  44. package/dist/deploy/agentcore/plan.d.ts +10 -11
  45. package/dist/deploy/agentcore/plan.js +8 -14
  46. package/dist/deploy/agentcore/run.d.ts +8 -1
  47. package/dist/deploy/agentcore/run.js +20 -14
  48. package/dist/deploy/agentcore/shell.d.ts +25 -0
  49. package/dist/deploy/agentcore/shell.js +202 -0
  50. package/dist/deploy/box-shell.d.ts +43 -0
  51. package/dist/deploy/box-shell.js +28 -0
  52. package/dist/deploy/build-context.js +16 -7
  53. package/dist/deploy/channel-ingress.d.ts +20 -1
  54. package/dist/deploy/channel-ingress.js +34 -7
  55. package/dist/deploy/container.d.ts +14 -0
  56. package/dist/deploy/container.js +45 -1
  57. package/dist/deploy/docker/plan.d.ts +7 -3
  58. package/dist/deploy/docker/plan.js +9 -14
  59. package/dist/deploy/docker/run.d.ts +14 -7
  60. package/dist/deploy/docker/run.js +58 -35
  61. package/dist/deploy/fly/plan.d.ts +6 -3
  62. package/dist/deploy/fly/plan.js +6 -8
  63. package/dist/deploy/fly/run.d.ts +8 -2
  64. package/dist/deploy/fly/run.js +15 -25
  65. package/dist/deploy/preflight.d.ts +9 -7
  66. package/dist/deploy/preflight.js +92 -20
  67. package/dist/deploy/railway/plan.d.ts +6 -3
  68. package/dist/deploy/railway/plan.js +7 -9
  69. package/dist/deploy/railway/run.d.ts +10 -6
  70. package/dist/deploy/railway/run.js +29 -13
  71. package/dist/deploy/runner.d.ts +16 -0
  72. package/dist/deploy/runner.js +31 -0
  73. package/dist/deploy/secrets.d.ts +4 -13
  74. package/dist/deploy/secrets.js +8 -27
  75. package/dist/dev-supervisor.js +7 -4
  76. package/dist/engines/pi/agent-models.d.ts +83 -0
  77. package/dist/engines/pi/agent-models.js +120 -0
  78. package/dist/engines/pi/agent-session-factory.d.ts +13 -13
  79. package/dist/engines/pi/agent-session-factory.js +132 -159
  80. package/dist/engines/pi/auth.d.ts +56 -3
  81. package/dist/engines/pi/auth.js +68 -109
  82. package/dist/engines/pi/config.d.ts +0 -25
  83. package/dist/engines/pi/config.js +3 -37
  84. package/dist/engines/pi/create.d.ts +34 -21
  85. package/dist/engines/pi/create.js +90 -57
  86. package/dist/engines/pi/entry-images.d.ts +14 -0
  87. package/dist/engines/pi/entry-images.js +23 -0
  88. package/dist/engines/pi/invoke-session.js +88 -20
  89. package/dist/engines/pi/locked-file.d.ts +9 -0
  90. package/dist/engines/pi/locked-file.js +102 -0
  91. package/dist/engines/pi/login.d.ts +27 -14
  92. package/dist/engines/pi/login.js +41 -18
  93. package/dist/engines/pi/machine.d.ts +23 -0
  94. package/dist/engines/pi/machine.js +24 -2
  95. package/dist/engines/pi/models.d.ts +120 -36
  96. package/dist/engines/pi/models.js +316 -66
  97. package/dist/engines/pi/open.d.ts +65 -20
  98. package/dist/engines/pi/open.js +88 -34
  99. package/dist/engines/pi/report.d.ts +3 -0
  100. package/dist/engines/pi/report.js +11 -0
  101. package/dist/engines/pi/service.d.ts +2 -2
  102. package/dist/engines/pi/service.js +1 -0
  103. package/dist/engines/pi/session-builder.d.ts +3 -2
  104. package/dist/engines/pi/session-builder.js +30 -76
  105. package/dist/engines/pi/session-control.d.ts +4 -4
  106. package/dist/engines/pi/session-control.js +86 -43
  107. package/dist/engines/pi/session-settings.d.ts +9 -0
  108. package/dist/engines/pi/session-settings.js +17 -0
  109. package/dist/engines/pi/session-store.d.ts +2 -2
  110. package/dist/engines/pi/tool-context.d.ts +3 -5
  111. package/dist/engines/pi/tool-context.js +4 -16
  112. package/dist/engines/pi/tool.d.ts +32 -21
  113. package/dist/engines/pi/tool.js +21 -25
  114. package/dist/engines/pi/turn-kit.d.ts +18 -9
  115. package/dist/engines/pi/turn-kit.js +115 -32
  116. package/dist/open-url.d.ts +15 -1
  117. package/dist/open-url.js +35 -3
  118. package/dist/paths.d.ts +4 -2
  119. package/dist/paths.js +6 -2
  120. package/dist/pi.d.ts +5 -3
  121. package/dist/pi.js +3 -2
  122. package/dist/service.d.ts +5 -8
  123. package/dist/session-remote.js +21 -0
  124. package/dist/session.d.ts +88 -15
  125. package/dist/session.js +5 -1
  126. package/package.json +6 -5
  127. package/dist/engines/pi/search-tools.d.ts +0 -4
  128. package/dist/engines/pi/search-tools.js +0 -109
@@ -1,4 +1,8 @@
1
+ /** The turn mechanism's ENGINE-agnostic half: the parts that describe a turn rather than pi. */
2
+ import { stripVTControlCharacters } from "node:util";
3
+ import { contentText } from "@earendil-works/pi-ai";
1
4
  import { ABORTED_CODE } from "../../agent.js";
5
+ import { log } from "../../log.js";
2
6
  export function inProcessLease() {
3
7
  const busy = new Set();
4
8
  return {
@@ -70,19 +74,38 @@ function messageSignal(message) {
70
74
  return {};
71
75
  }
72
76
  /**
73
- * Terminal mapping, decided by the resolved message's stopReason: pi's `prompt()` RESOLVES a message with stopReason
77
+ * How an answer ended, read off the stopReason pi records on the message. The ONE reading: `message_finished`,
78
+ * the `assistant` entry `entries()` publishes, and the invoke terminal all derive from it, so the live outcome and
79
+ * the recorded one cannot disagree.
80
+ */
81
+ export function answerOutcome(message) {
82
+ const error = message.errorMessage ? { error: { message: message.errorMessage } } : {};
83
+ switch (message.stopReason) {
84
+ case "error":
85
+ return { status: "failed", ...error };
86
+ case "aborted":
87
+ return { status: "aborted", ...error };
88
+ case "length":
89
+ return { status: "truncated" };
90
+ default:
91
+ return undefined;
92
+ }
93
+ }
94
+ /**
95
+ * Terminal mapping, decided by the resolved message's outcome: pi's `prompt()` RESOLVES a message with stopReason
74
96
  * "error"/"aborted" rather than throwing, so relying on catch alone would miss that whole failure class and violate
75
- * SPEC MUST 1.
97
+ * SPEC MUST 1. A truncated final answer still completes the run.
76
98
  */
77
99
  export function toTerminal(message) {
78
- if (message.stopReason === "aborted") {
100
+ const outcome = answerOutcome(message);
101
+ if (outcome?.status === "aborted") {
79
102
  // A deliberate stop (a control-plane or consumer abort), not an error — see {@link ABORTED_CODE} for the consumer
80
103
  // contract (design §6).
81
- const details = message.errorMessage ?? "run aborted";
104
+ const details = outcome.error?.message ?? "run aborted";
82
105
  return { type: "failed", details, retryable: false, code: ABORTED_CODE };
83
106
  }
84
- if (message.stopReason === "error") {
85
- const details = message.errorMessage ?? `model stopped: ${message.stopReason}`;
107
+ if (outcome?.status === "failed") {
108
+ const details = outcome.error?.message ?? "model stopped: error";
86
109
  return { type: "failed", details, retryable: classifyRetryable(details, messageSignal(message)) };
87
110
  }
88
111
  return { type: "completed" };
@@ -105,35 +128,95 @@ export async function toPiPromptOptions(prompt, delivery) {
105
128
  return { images: prompt.images.map((img) => ({ type: "image", data: img.data, mimeType: img.mimeType })) };
106
129
  }
107
130
  const { resizeImage } = await import("@earendil-works/pi-coding-agent");
108
- const images = await Promise.all(prompt.images.map(async (img) => {
109
- const resized = await resizeImage(Buffer.from(img.data, "base64"), img.mimeType).catch(() => null);
110
- return resized
111
- ? { type: "image", data: resized.data, mimeType: resized.mimeType }
112
- : { type: "image", data: img.data, mimeType: img.mimeType };
131
+ const images = await Promise.all(prompt.images.map(async (img, i) => {
132
+ const resized = await resizeImage(Buffer.from(img.data, "base64"), img.mimeType);
133
+ if (resized)
134
+ return { type: "image", data: resized.data, mimeType: resized.mimeType };
135
+ // `null` covers an image it cannot decode or shrink under the limit AND an image backend that did not load,
136
+ // and does not say which. pi's own tool-result path keeps the original in that case rather than drop what the
137
+ // sender attached, and so does this; a provider that refuses it fails the turn with its own message.
138
+ log.warn(`[fastagent] queued image ${i + 1} (${img.mimeType}) could not be resized; sending it as given — ` +
139
+ "the provider may refuse it");
140
+ return { type: "image", data: img.data, mimeType: img.mimeType };
113
141
  }));
114
142
  return { images };
115
143
  }
116
- /** The SPEC projection of the rich stream. */
117
- export function projectAgentEvent(se) {
118
- switch (se.type) {
119
- case "message_delta": {
120
- const d = se.data;
121
- return d.channel === "text" ? { type: "text", delta: d.delta } : { type: "thinking", delta: d.delta };
122
- }
123
- case "tool_started": {
124
- const d = se.data;
125
- return { type: "tool_started", id: d.id, name: d.name, args: d.args };
126
- }
127
- case "tool_finished": {
128
- const d = se.data;
129
- return { type: "tool_ended", id: d.id, isError: d.isError, content: d.content };
130
- }
131
- case "retry_scheduled": {
132
- // `operation` (compaction | branch_summary) stays session-plane vocabulary.
133
- const d = se.data;
134
- return { type: "retrying", attempt: d.attempt, maxAttempts: d.maxAttempts, delayMs: d.delayMs, reason: d.error };
135
- }
136
- default:
144
+ /**
145
+ * The SPEC projection of ONE run's rich stream. Stateful only for `tool_progress`, the one event that is a status
146
+ * rather than a fact: an outer call's status line is sent when it changes, and a call that call makes, at any depth,
147
+ * is its status. Nested calls are otherwise the observation plane's alone.
148
+ */
149
+ export function agentEventProjection() {
150
+ /** Each nested call's outer call, the one the stream announced with `tool_started`. */
151
+ const outerOf = new Map();
152
+ /** The status line each outer call last reported. */
153
+ const shown = new Map();
154
+ const progress = (id, text) => {
155
+ if (text === undefined || shown.get(id) === text)
137
156
  return null;
157
+ shown.set(id, text);
158
+ return { type: "tool_progress", id, text };
159
+ };
160
+ return (se) => {
161
+ switch (se.type) {
162
+ case "message_delta": {
163
+ const d = se.data;
164
+ return d.channel === "text" ? { type: "text", delta: d.delta } : { type: "thinking", delta: d.delta };
165
+ }
166
+ case "tool_started": {
167
+ const d = se.data;
168
+ if (!d.parentToolCallId)
169
+ return { type: "tool_started", id: d.id, name: d.name, args: d.args };
170
+ const outer = outerOf.get(d.parentToolCallId) ?? d.parentToolCallId;
171
+ outerOf.set(d.id, outer);
172
+ return progress(outer, callLine(d.name, d.args));
173
+ }
174
+ case "tool_progress": {
175
+ const d = se.data;
176
+ // A nested call's own output is not its caller's status; the nested call starting is (above).
177
+ if (d.parentToolCallId || !Array.isArray(d.partialResult.content))
178
+ return null;
179
+ return progress(d.id, lastLine(contentText(d.partialResult.content, "\n")));
180
+ }
181
+ case "tool_finished": {
182
+ const d = se.data;
183
+ if (d.parentToolCallId)
184
+ return null;
185
+ return { type: "tool_ended", id: d.id, isError: d.isError, content: d.content };
186
+ }
187
+ case "retry_scheduled": {
188
+ // `operation` (compaction | branch_summary) stays session-plane vocabulary.
189
+ const d = se.data;
190
+ return {
191
+ type: "retrying",
192
+ attempt: d.attempt,
193
+ maxAttempts: d.maxAttempts,
194
+ delayMs: d.delayMs,
195
+ reason: d.error,
196
+ };
197
+ }
198
+ default:
199
+ return null;
200
+ }
201
+ };
202
+ }
203
+ /**
204
+ * The line a terminal shows last: control sequences removed, and a bare `\r`, a progress bar redrawing in place, ends a
205
+ * line like `\n` does. Undefined when there is no visible text yet (a shell that has printed nothing).
206
+ */
207
+ function lastLine(text) {
208
+ const lines = stripVTControlCharacters(text).split(/\r\n|\r|\n/);
209
+ for (let i = lines.length - 1; i >= 0; i--) {
210
+ const line = lines[i]?.trim();
211
+ if (line)
212
+ return line;
138
213
  }
214
+ return undefined;
215
+ }
216
+ /** A nested call as its caller's status: the tool and its first plain argument, on one line (`weather London`). */
217
+ function callLine(name, args) {
218
+ const first = args !== null && typeof args === "object" && !Array.isArray(args)
219
+ ? Object.values(args).find((value) => typeof value === "string" || typeof value === "number")
220
+ : undefined;
221
+ return first === undefined ? name : `${name} ${String(first).replace(/\s+/g, " ").trim()}`.trim();
139
222
  }
@@ -1,2 +1,16 @@
1
- /** Best-effort open a URL in the default browser. */
1
+ /**
2
+ * How to hand `url` to the default browser on `platform`, or undefined when it must not be handed over at all.
3
+ *
4
+ * Only a web page: `https:`, or `http:` on a loopback host. Some callers pass URLs they did not build — a deployed box's
5
+ * login relays its own (`cli/login-relay.ts`), and that box runs code its agent can rewrite — and the platform openers
6
+ * open anything: a local path, a `file:`/`smb:` URL, an argument that starts with `-`. Parsed and re-serialized, so
7
+ * what reaches the opener is what was checked. No shell anywhere: on Windows `start` needs one, and cmd reads the `&`
8
+ * every OAuth URL carries as a command separator, so `rundll32 url.dll,FileProtocolHandler` takes the URL as one
9
+ * argument instead.
10
+ */
11
+ export declare function browserCommand(url: string, platform: NodeJS.Platform): {
12
+ cmd: string;
13
+ args: string[];
14
+ } | undefined;
15
+ /** Best-effort open a URL in the default browser (callers print it too). A URL that is not a web page is refused, visibly. */
2
16
  export declare function openExternalUrl(url: string): void;
package/dist/open-url.js CHANGED
@@ -1,6 +1,38 @@
1
1
  import { spawn } from "node:child_process";
2
- /** Best-effort open a URL in the default browser. */
2
+ /**
3
+ * How to hand `url` to the default browser on `platform`, or undefined when it must not be handed over at all.
4
+ *
5
+ * Only a web page: `https:`, or `http:` on a loopback host. Some callers pass URLs they did not build — a deployed box's
6
+ * login relays its own (`cli/login-relay.ts`), and that box runs code its agent can rewrite — and the platform openers
7
+ * open anything: a local path, a `file:`/`smb:` URL, an argument that starts with `-`. Parsed and re-serialized, so
8
+ * what reaches the opener is what was checked. No shell anywhere: on Windows `start` needs one, and cmd reads the `&`
9
+ * every OAuth URL carries as a command separator, so `rundll32 url.dll,FileProtocolHandler` takes the URL as one
10
+ * argument instead.
11
+ */
12
+ export function browserCommand(url, platform) {
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(url);
16
+ }
17
+ catch {
18
+ return undefined; // not a URL: nothing to open
19
+ }
20
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
21
+ if (!(parsed.protocol === "https:" || (parsed.protocol === "http:" && loopback)))
22
+ return undefined;
23
+ const href = parsed.href;
24
+ if (platform === "darwin")
25
+ return { cmd: "open", args: [href] };
26
+ if (platform === "win32")
27
+ return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", href] };
28
+ return { cmd: "xdg-open", args: [href] };
29
+ }
30
+ /** Best-effort open a URL in the default browser (callers print it too). A URL that is not a web page is refused, visibly. */
3
31
  export function openExternalUrl(url) {
4
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5
- spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => { });
32
+ const command = browserCommand(url, process.platform);
33
+ if (!command) {
34
+ console.error(`[fastagent] not opening ${JSON.stringify(url)}: only an https URL, or http on this machine, opens`);
35
+ return;
36
+ }
37
+ spawn(command.cmd, command.args, { stdio: "ignore", detached: true }).on("error", () => { });
6
38
  }
package/dist/paths.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** The directory name `init` gives an agent (`<workspace>/fastagent/`) unless `--agent-dir` names another. */
2
2
  export declare const DEFAULT_AGENT_DIRNAME = "fastagent";
3
- /** The user-global machinery home under `$HOME`. */
4
- export declare const GLOBAL_HOME_DIR = ".fastagent";
3
+ /** The user-global machinery home, `~/.fastagent`: the machine's credentials, models.json and model catalog. */
4
+ export declare function globalHome(): string;
5
5
  /**
6
6
  * The secrets segment inside an agent dir (or the global home). Every path fastagent resolves — `.env`,
7
7
  * `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift apart.
@@ -19,6 +19,8 @@ export declare const STATE_DIRNAME = ".state";
19
19
  export declare const AGENT_CONFIG_FILE = "fastagent.config.ts";
20
20
  /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). */
21
21
  export declare const AGENT_MODELS_FILE = "models.json";
22
+ /** The agent's model catalog: pi.dev's models newer than pi's bundled catalog, fetched by `models --refresh`. */
23
+ export declare const AGENT_MODEL_CATALOG_FILE = "models-store.json";
22
24
  export interface ResolvedPlacement {
23
25
  /**
24
26
  * The AGENT directory — where the definition (persona.md/skills/tools/channels/routines), the config, and the
package/dist/paths.js CHANGED
@@ -8,8 +8,10 @@ import { homedir } from "node:os";
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
9
  /** The directory name `init` gives an agent (`<workspace>/fastagent/`) unless `--agent-dir` names another. */
10
10
  export const DEFAULT_AGENT_DIRNAME = "fastagent";
11
- /** The user-global machinery home under `$HOME`. */
12
- export const GLOBAL_HOME_DIR = ".fastagent";
11
+ /** The user-global machinery home, `~/.fastagent`: the machine's credentials, models.json and model catalog. */
12
+ export function globalHome() {
13
+ return join(homedir(), ".fastagent");
14
+ }
13
15
  /**
14
16
  * The secrets segment inside an agent dir (or the global home). Every path fastagent resolves — `.env`,
15
17
  * `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift apart.
@@ -27,6 +29,8 @@ export const STATE_DIRNAME = ".state";
27
29
  export const AGENT_CONFIG_FILE = "fastagent.config.ts";
28
30
  /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). */
29
31
  export const AGENT_MODELS_FILE = "models.json";
32
+ /** The agent's model catalog: pi.dev's models newer than pi's bundled catalog, fetched by `models --refresh`. */
33
+ export const AGENT_MODEL_CATALOG_FILE = "models-store.json";
30
34
  /**
31
35
  * The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded inside
32
36
  * ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
package/dist/pi.d.ts CHANGED
@@ -5,7 +5,7 @@ export { z } from "zod";
5
5
  export type { AgentTool, ExecutionEnv, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
6
6
  /** A conversation record, as the tool runtime and the control plane hold it. */
7
7
  export type { SessionManager, SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
8
- export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, } from "./engines/pi/open.ts";
8
+ export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, refreshMachineModelCatalog, refreshModelCatalog, } from "./engines/pi/open.ts";
9
9
  export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
10
10
  export { defineConfig, listModels, resolveModel, type FastagentConfig, } from "./engines/pi/config.ts";
11
11
  export type { SessionObserver } from "./engines/pi/turn-kit.ts";
@@ -14,9 +14,11 @@ export { createPiSessionControl, type CreatePiSessionControlOptions, } from "./e
14
14
  export { piInMemorySessionRecordStore, piSessionRecordStore, type PiSessionRecordStore, } from "./engines/pi/session-store.ts";
15
15
  export type { SessionInheritance } from "./engines/pi/session-inheritance.ts";
16
16
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore, type FastagentAuthOptions } from "./engines/pi/auth.ts";
17
- export { createPiModels, probeAuthSource, type CreatePiModelsOptions } from "./engines/pi/models.ts";
17
+ export { createPiModels, type CreatePiModelsOptions } from "./engines/pi/agent-models.ts";
18
+ export { probeAuthSource } from "./engines/pi/models.ts";
18
19
  export { login, loginOptions, LoginCancelled, type LoginMethod, type LoginOption, type LoginRequest, type LoginResult, } from "./engines/pi/login.ts";
19
- export type { AuthEvent, AuthInteraction, AuthPrompt } from "@earendil-works/pi-ai";
20
+ /** pi-ai's sign-in types, and the `CredentialStore` a caller supplies as `credentialStore` (with what it holds). */
21
+ export type { AuthEvent, AuthInteraction, AuthPrompt, Credential, CredentialStore } from "@earendil-works/pi-ai";
20
22
  export type { Models } from "@earendil-works/pi-ai";
21
23
  export type { Provider, ProviderAuth } from "@earendil-works/pi-ai";
22
24
  export type { Model } from "@earendil-works/pi-ai";
package/dist/pi.js CHANGED
@@ -2,13 +2,14 @@
2
2
  export { createPiAgent, createPiAgentFromDefinition, } from "./engines/pi/create.js";
3
3
  export { defineTool, } from "./engines/pi/tool.js";
4
4
  export { z } from "zod";
5
- export { availableModelsFromDir, createPiAgentFromDir, } from "./engines/pi/open.js";
5
+ export { availableModelsFromDir, createPiAgentFromDir, refreshMachineModelCatalog, refreshModelCatalog, } from "./engines/pi/open.js";
6
6
  export { defineConfig, listModels, resolveModel, } from "./engines/pi/config.js";
7
7
  export { inProcessLease } from "./engines/pi/turn-kit.js";
8
8
  export { createPiSessionControl, } from "./engines/pi/session-control.js";
9
9
  export { piInMemorySessionRecordStore, piSessionRecordStore, } from "./engines/pi/session-store.js";
10
10
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore } from "./engines/pi/auth.js";
11
- export { createPiModels, probeAuthSource } from "./engines/pi/models.js";
11
+ export { createPiModels } from "./engines/pi/agent-models.js";
12
+ export { probeAuthSource } from "./engines/pi/models.js";
12
13
  export { login, loginOptions, LoginCancelled, } from "./engines/pi/login.js";
13
14
  // The product's one-call assembly: a directory becomes a live service.
14
15
  export { createAgentService } from "./engines/pi/service.js";
package/dist/service.d.ts CHANGED
@@ -7,9 +7,9 @@ import type { LoadedRoutine } from "./schedule/routine.ts";
7
7
  export interface ServingSurface {
8
8
  /**
9
9
  * The routes that AUTHENTICATE NOBODY — `GET /health`, `POST /invoke`, and `POST /run` where the definition
10
- * declares routines, minus what a channel took over or `http.invoke: false` withheld. Kept APART from the channels' rather than merged with a list of which keys are
11
- * which: three separate decisions read this (browser reachability, the JSON body gate, what the startup line
12
- * calls unauthenticated), and while it was a derived list each of them could answer differently.
10
+ * declares routines, minus what a channel took over or `http.invoke: false` withheld. Kept APART from the
11
+ * channels' table rather than derived from it, because three decisions read it (browser reachability, the JSON
12
+ * body gate, what the startup line calls unauthenticated) and must give the same answer.
13
13
  */
14
14
  unverified: Routes;
15
15
  /**
@@ -133,11 +133,8 @@ export interface AgentService {
133
133
  longConnections: string[];
134
134
  };
135
135
  /**
136
- * The route keys on this port that AUTHENTICATE NOBODY — `GET /health`, `POST /invoke`, and `POST /run` where
137
- * the definition declares routines, minus what a channel took over or `http.invoke: false` withheld. The FACT a caller needs to describe this surface: reading it off
138
- * `routes` instead answers a different question, since a channel may serve one of those paths with a protocol of
139
- * its own. That mistake ran in both directions here — a try-it curl for a route that 404s, and a warning about an
140
- * unauthenticated `/invoke` that was really a signature-checked channel.
136
+ * The keys of {@link ServingSurface.unverified}: what a caller reads to describe this surface. Not derivable from
137
+ * `routes`, where a channel may serve one of those paths behind its own signature check.
141
138
  */
142
139
  unverifiedRoutes: readonly string[];
143
140
  /**
@@ -271,6 +271,19 @@ export async function connectSessionControl(options) {
271
271
  id: session,
272
272
  state: () => get(`/control/sessions/${id(session)}`),
273
273
  entries: (options) => get(`/control/sessions/${id(session)}/entries${options?.since !== undefined ? `?since=${encodeURIComponent(options.since)}` : ""}`, PAYLOAD_TIMEOUT_MS),
274
+ image: async (ref) => {
275
+ const res = await fetchFn(`${base}/control/sessions/${id(session)}/image?ref=${encodeURIComponent(ref)}`, {
276
+ signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
277
+ });
278
+ if (!res.ok)
279
+ throw await controlError(res);
280
+ if (res.status === 204)
281
+ return undefined;
282
+ return {
283
+ data: toBase64(new Uint8Array(await res.arrayBuffer())),
284
+ mimeType: res.headers.get("content-type") ?? "application/octet-stream",
285
+ };
286
+ },
274
287
  events: () => eventsOf(session),
275
288
  update: (patch) => write(`/control/sessions/${id(session)}`, "PATCH", patch),
276
289
  steer: (prompt) => write(`/control/sessions/${id(session)}/actions`, "POST", { type: "steer", prompt }),
@@ -404,6 +417,14 @@ export function connectAgent(options) {
404
417
  },
405
418
  };
406
419
  }
420
+ /** Base64 without `Buffer` (this module runs in any Fetch runtime), in chunks: spreading megabytes of bytes into one
421
+ * `String.fromCharCode` call overflows the argument limit. */
422
+ function toBase64(bytes) {
423
+ let binary = "";
424
+ for (let i = 0; i < bytes.length; i += 0x8000)
425
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
426
+ return btoa(binary);
427
+ }
407
428
  /** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other fields. */
408
429
  async function* sseData(body, watch) {
409
430
  const decoder = new TextDecoder();
package/dist/session.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session control plane — the engine-neutral serving extension beside Agent Handler (docs/design/session-control.md).
3
3
  */
4
- import type { Json, Prompt } from "./agent.ts";
4
+ import type { ImageRef, Json, Prompt } from "./agent.ts";
5
5
  export interface SessionControl {
6
6
  capabilities(): SessionCapabilities;
7
7
  /** The names this agent exposes — what a composer's `/` completion LISTS. */
@@ -42,15 +42,34 @@ export interface Session {
42
42
  entries(options?: {
43
43
  since?: string;
44
44
  }): Promise<SessionEntries>;
45
+ /**
46
+ * The bytes behind an {@link EntryImage.ref} this session published; `undefined` when this session holds no such
47
+ * image. Total, like every read but `list()`.
48
+ *
49
+ * Over the wire, a `mimeType` other than `image/png`, `image/jpeg`, `image/gif` or `image/webp` reads back as
50
+ * `application/octet-stream`: the transport will not serve a sender-declared type it cannot trust (the entry still
51
+ * lists the declared one). pi normalizes an invoke's opening prompt images to those four; a `steer`/`followUp`
52
+ * image and a tool result keep whatever type the sender declared, so `image/jpg`, a type with parameters, or
53
+ * `image/svg+xml` sent that way reads back as `application/octet-stream` remotely.
54
+ */
55
+ image(ref: string): Promise<ImageRef | undefined>;
45
56
  events(): SessionEventStream;
46
- /** Set durable session properties. */
57
+ /**
58
+ * Set durable session properties, applied by the session's next turn. An id with no record (nothing has written it
59
+ * yet) takes them too: the write creates its record, and the session's first turn runs on them. Refused there:
60
+ * `leafEntryId` (no entries to point at) and an id no client could address ({@link isAddressableSession}).
61
+ */
47
62
  update(patch: SessionUpdate): Promise<SessionResult>;
48
63
  /**
49
64
  * Join the active run: delivered after the current turn's tool calls, before the next model call. `prompt.text`
50
- * must not be empty (`invalid_command`): a queued prompt is reported by its text ({@link PendingPrompts}).
65
+ * must not be empty (`invalid_command`): a queued prompt is reported by its text ({@link PendingPrompts}). An
66
+ * accepted result carries its {@link PromptDisposition}.
51
67
  */
52
68
  steer(prompt: Prompt): Promise<SessionResult>;
53
- /** Queue for the active run, FIFO, delivered when it is otherwise idle. Same non-empty `text` rule as `steer`. */
69
+ /**
70
+ * Queue for the active run, FIFO, delivered when it is otherwise idle. Same non-empty `text` rule and
71
+ * {@link PromptDisposition} as `steer`.
72
+ */
54
73
  followUp(prompt: Prompt): Promise<SessionResult>;
55
74
  /** Stop the active run — its queues, its retry delay, its cancellable tool work. */
56
75
  abort(): Promise<SessionResult>;
@@ -110,11 +129,22 @@ export interface SessionCapabilities {
110
129
  delete: boolean;
111
130
  /** Which {@link SessionUpdate} fields this deployment accepts. */
112
131
  updatable: SessionUpdateField[];
113
- /** The specs `update({ model })` accepts — present iff `model` is updatable. */
114
- allowedModels?: string[];
132
+ /** The models `update({ model })` accepts, by `spec` — present iff `model` is updatable. */
133
+ allowedModels?: ModelDescriptor[];
115
134
  toolProgress: boolean;
116
135
  usage: boolean;
117
136
  }
137
+ /**
138
+ * A model as a picker shows it, before any session runs on it. `thinkingLevels` is what `update({ thinkingLevel })`
139
+ * accepts for a session on this model; `name` and `contextWindow` are absent when the model does not declare them.
140
+ */
141
+ export interface ModelDescriptor {
142
+ /** What `update({ model })` takes. */
143
+ spec: string;
144
+ name?: string;
145
+ thinkingLevels: string[];
146
+ contextWindow?: number;
147
+ }
118
148
  /** One name a client can offer the user. */
119
149
  export interface AgentCommand {
120
150
  name: string;
@@ -133,7 +163,11 @@ export declare const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
133
163
  export declare const NO_ACTIVE_RUN_CODE = "no_active_run";
134
164
  /** Stable `SessionResult.error.code` for a PAYLOAD that is invalid for this runtime. */
135
165
  export declare const INVALID_COMMAND_CODE = "invalid_command";
136
- /** Stable `SessionResult.error.code` for a write against a session that does not exist. */
166
+ /**
167
+ * Stable `SessionResult.error.code` for a write that needs a record (fork's source, compact, delete) against an id
168
+ * that has none, because nothing has written it yet. `update()` creates the record instead, and answers this code only
169
+ * when a concurrent `delete()` removes the record between its read and its write.
170
+ */
137
171
  export declare const NO_SUCH_SESSION_CODE = "no_such_session";
138
172
  /** Stable `SessionResult.error.code` for a write rejected BEFORE acceptance with nothing durable landed. */
139
173
  export declare const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
@@ -169,6 +203,7 @@ export declare const PARTIAL_UPDATE_CODE = "partial_update";
169
203
  export type SessionResult = {
170
204
  ok: true;
171
205
  runId?: string;
206
+ disposition?: PromptDisposition;
172
207
  } | {
173
208
  ok: false;
174
209
  error: {
@@ -177,6 +212,13 @@ export type SessionResult = {
177
212
  retryable: boolean;
178
213
  };
179
214
  };
215
+ /**
216
+ * What became of an accepted `steer`/`followUp` prompt — present on exactly those results. `queued`: it waits in
217
+ * {@link PendingPrompts} until the run takes it in. `handled`: the engine consumed it before the queue (in pi, a
218
+ * definition extension's `input` handler), so it never enters the conversation, and no `queue_changed` or
219
+ * `user_message` reports it.
220
+ */
221
+ export type PromptDisposition = "queued" | "handled";
180
222
  /** One session in {@link SessionControl.sessions} — a conversation-list row, not a session's contents. */
181
223
  export interface SessionSummary {
182
224
  session: string;
@@ -194,7 +236,10 @@ export interface SessionState {
194
236
  /** `compacting` refers to MANUAL compaction (`compact`) at a session boundary. */
195
237
  status: "idle" | "running" | "compacting";
196
238
  activeRunId?: string;
197
- /** What this session will RUN with, not what was recorded. */
239
+ /**
240
+ * What this session will RUN with, not what was recorded. An id with no record reports what its first turn would run
241
+ * on: the defaults, the level clamped to the model.
242
+ */
198
243
  model?: string;
199
244
  thinkingLevel?: string;
200
245
  /** What `update({ thinkingLevel })` accepts for THIS session — re-read after a model change. */
@@ -232,6 +277,15 @@ export interface SessionEntry {
232
277
  kind: string;
233
278
  data: Json;
234
279
  }
280
+ /**
281
+ * One image a message carried, as `data.images` lists it on a `user` or `tool` entry and on `user_message` (absent
282
+ * when there are none). Bytes stay out: `entries()` is read on every open and reconnect, and one screenshot is
283
+ * megabytes. `ref` is opaque; {@link Session.image} of the same session reads the bytes.
284
+ */
285
+ export type EntryImage = {
286
+ ref: string;
287
+ mimeType: string;
288
+ };
235
289
  /**
236
290
  * A live event subscription, plus the ONE thing a reconnecting client cannot infer: when it started.
237
291
  *
@@ -283,12 +337,13 @@ export type MessageStartedEvent = SessionEvent<"message_started", Record<never,
283
337
  /**
284
338
  * A user message entered the conversation: the run's opening prompt, a steer or follow-up leaving `pending`, or one an
285
339
  * extension sent. It reports after the message is recorded, so `entries()` already holds `entryId` with the same
286
- * `text` (a slash command expanded, images not included), and before the answer to it starts. The one live event a
287
- * backfill can be deduplicated against exactly.
340
+ * `text` (a slash command expanded) and `images`, and before the answer to it starts. The one live event a backfill
341
+ * can be deduplicated against exactly.
288
342
  */
289
343
  export type UserMessageEvent = SessionEvent<"user_message", {
290
344
  entryId: string;
291
345
  text: string;
346
+ images?: EntryImage[];
292
347
  }> & {
293
348
  runId: string;
294
349
  };
@@ -298,13 +353,29 @@ export type MessageDeltaEvent = SessionEvent<"message_delta", {
298
353
  }> & {
299
354
  runId: string;
300
355
  };
301
- export type MessageFinishedEvent = SessionEvent<"message_finished", Record<never, never>> & {
356
+ /**
357
+ * How an answer ended, when it did not end normally: absent on an answer that did. One value, carried live by its
358
+ * `message_finished` and durably by its `assistant` entry, so a reopened conversation reads what a watcher saw.
359
+ * `truncated` is an answer cut off at the output limit — a property of the answer, not of its run, which may still
360
+ * complete. An answer whose entry a `context_edit { omitted }` targets (`failed` or `truncated`) is an attempt the
361
+ * engine abandoned; a retry usually, but not always, follows it.
362
+ */
363
+ export type AnswerOutcome = {
364
+ status: "failed" | "aborted" | "truncated";
365
+ error?: {
366
+ message: string;
367
+ };
368
+ };
369
+ export type MessageFinishedEvent = SessionEvent<"message_finished", {
370
+ outcome?: AnswerOutcome;
371
+ }> & {
302
372
  runId: string;
303
373
  };
304
374
  export type ToolStartedEvent = SessionEvent<"tool_started", {
305
375
  id: string;
306
376
  name: string;
307
377
  args: Json;
378
+ parentToolCallId?: string;
308
379
  }> & {
309
380
  runId: string;
310
381
  };
@@ -313,6 +384,7 @@ export type ToolProgressEvent = SessionEvent<"tool_progress", {
313
384
  id: string;
314
385
  name: string;
315
386
  partialResult: Json;
387
+ parentToolCallId?: string;
316
388
  }> & {
317
389
  runId: string;
318
390
  };
@@ -320,15 +392,16 @@ export type ToolFinishedEvent = SessionEvent<"tool_finished", {
320
392
  id: string;
321
393
  isError: boolean;
322
394
  content: Json;
395
+ parentToolCallId?: string;
323
396
  }> & {
324
397
  runId: string;
325
398
  };
326
399
  /**
327
400
  * The prompts queued on the active run, oldest first, as the engine queued them: plain text as sent, a slash command
328
- * already expanded (a `/skill:…` becomes the skill's text), images not included. A prompt LEAVES its list when it
329
- * enters the conversation as a user message: from then on it is in the record and in every later model call (an abort
330
- * can still end the run before the model answers it). Whatever is still listed when the run settles never entered
331
- * the conversation and is dropped with the run.
401
+ * already expanded (a `/skill:…` becomes the skill's text), without its images (its `user_message` carries them). A
402
+ * prompt LEAVES its list when it enters the conversation as a user message: from then on it is in the record and in
403
+ * every later model call (an abort can still end the run before the model answers it). Whatever is still listed when
404
+ * the run settles never entered the conversation and is dropped with the run.
332
405
  */
333
406
  export type PendingPrompts = {
334
407
  steering: string[];
package/dist/session.js CHANGED
@@ -28,7 +28,11 @@ export const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
28
28
  export const NO_ACTIVE_RUN_CODE = "no_active_run";
29
29
  /** Stable `SessionResult.error.code` for a PAYLOAD that is invalid for this runtime. */
30
30
  export const INVALID_COMMAND_CODE = "invalid_command";
31
- /** Stable `SessionResult.error.code` for a write against a session that does not exist. */
31
+ /**
32
+ * Stable `SessionResult.error.code` for a write that needs a record (fork's source, compact, delete) against an id
33
+ * that has none, because nothing has written it yet. `update()` creates the record instead, and answers this code only
34
+ * when a concurrent `delete()` removes the record between its read and its write.
35
+ */
32
36
  export const NO_SUCH_SESSION_CODE = "no_such_session";
33
37
  /** Stable `SessionResult.error.code` for a write rejected BEFORE acceptance with nothing durable landed. */
34
38
  export const BOUNDARY_COMMAND_FAILED_CODE = "boundary_command_failed";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",
@@ -101,15 +101,16 @@
101
101
  },
102
102
  "dependencies": {
103
103
  "@clack/prompts": "^1.6.0",
104
- "@earendil-works/pi-agent-core": "^0.87.1",
105
- "@earendil-works/pi-ai": "^0.87.1",
106
- "@earendil-works/pi-coding-agent": "^0.87.1",
104
+ "@earendil-works/pi-agent-core": "^0.99.2",
105
+ "@earendil-works/pi-ai": "^0.99.2",
106
+ "@earendil-works/pi-coding-agent": "^0.99.2",
107
107
  "@hono/node-server": "^2.1.1",
108
108
  "@larksuiteoapi/node-sdk": "^1.71.1",
109
+ "@smithy/signature-v4": "^5.7.4",
109
110
  "chokidar": "^5.0.0",
110
111
  "commander": "^15.0.0",
111
112
  "croner": "^10.0.1",
112
- "effect": "4.0.0-rc.117",
113
+ "effect": "4.0.0-rc.118",
113
114
  "giget": "^3.3.0",
114
115
  "ignore": "^7.0.5",
115
116
  "proper-lockfile": "^4.1.2",