@fastagent-sh/fastagent 0.17.0 → 0.18.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 (68) hide show
  1. package/dist/agent.d.ts +11 -0
  2. package/dist/channels/agentcore.d.ts +16 -2
  3. package/dist/channels/agentcore.js +66 -4
  4. package/dist/channels/feishu/card.d.ts +20 -9
  5. package/dist/channels/feishu/card.js +27 -13
  6. package/dist/channels/feishu/feishu-api.d.ts +13 -2
  7. package/dist/channels/feishu/feishu.js +121 -17
  8. package/dist/channels/feishu/invoke-turn.d.ts +12 -2
  9. package/dist/channels/feishu/invoke-turn.js +159 -14
  10. package/dist/channels/feishu/normalize.js +97 -32
  11. package/dist/channels/feishu/parse.js +6 -0
  12. package/dist/channels/feishu/preview.d.ts +4 -3
  13. package/dist/channels/feishu/preview.js +77 -23
  14. package/dist/channels/feishu/scaffold/feishu-send.ts +9 -6
  15. package/dist/channels/http.js +15 -2
  16. package/dist/channels/invoke-turn-kit.d.ts +5 -2
  17. package/dist/channels/invoke-turn-kit.js +6 -2
  18. package/dist/channels/lark/scaffold/lark-send.ts +9 -6
  19. package/dist/channels/slack/invoke-turn.js +1 -1
  20. package/dist/channels/slack/slack.js +1 -5
  21. package/dist/channels/state.d.ts +0 -10
  22. package/dist/channels/state.js +2 -19
  23. package/dist/channels/telegram/invoke-turn.js +1 -1
  24. package/dist/channels/thread-participants.d.ts +7 -0
  25. package/dist/channels/thread-participants.js +3 -0
  26. package/dist/cli/commands/deploy.js +13 -5
  27. package/dist/cli/commands/dev.js +1 -1
  28. package/dist/cli/commands/fire.js +1 -1
  29. package/dist/cli/commands/info.js +21 -1
  30. package/dist/cli/commands/invoke.js +1 -1
  31. package/dist/cli/commands/start.js +40 -6
  32. package/dist/cli/serve.d.ts +4 -0
  33. package/dist/cli/serve.js +2 -2
  34. package/dist/cli/shared.d.ts +4 -2
  35. package/dist/cli/shared.js +12 -5
  36. package/dist/collect.d.ts +30 -4
  37. package/dist/collect.js +39 -6
  38. package/dist/deploy/agentcore/plan.js +17 -0
  39. package/dist/deploy/agentcore/run.d.ts +7 -1
  40. package/dist/deploy/agentcore/run.js +93 -8
  41. package/dist/deploy/preflight.d.ts +8 -2
  42. package/dist/deploy/preflight.js +55 -10
  43. package/dist/deploy/secrets.d.ts +3 -0
  44. package/dist/deploy/secrets.js +6 -0
  45. package/dist/dev-supervisor.js +8 -2
  46. package/dist/engines/pi/create.d.ts +2 -1
  47. package/dist/engines/pi/create.js +12 -7
  48. package/dist/engines/pi/harness.d.ts +6 -3
  49. package/dist/engines/pi/harness.js +4 -3
  50. package/dist/engines/pi/invoke-session.d.ts +32 -0
  51. package/dist/engines/pi/invoke-session.js +171 -0
  52. package/dist/engines/pi/invoke.d.ts +6 -27
  53. package/dist/engines/pi/invoke.js +49 -208
  54. package/dist/engines/pi/models.d.ts +45 -11
  55. package/dist/engines/pi/models.js +55 -8
  56. package/dist/engines/pi/session-builder.js +4 -2
  57. package/dist/engines/pi/session-control.d.ts +2 -1
  58. package/dist/engines/pi/sessions.d.ts +17 -1
  59. package/dist/engines/pi/sessions.js +292 -10
  60. package/dist/engines/pi/turn-kit.d.ts +56 -0
  61. package/dist/engines/pi/turn-kit.js +161 -0
  62. package/dist/paths.d.ts +6 -0
  63. package/dist/paths.js +6 -0
  64. package/dist/pi.d.ts +3 -2
  65. package/dist/pi.js +1 -1
  66. package/dist/scaffold/templates/fastagent.config.mjs +2 -0
  67. package/dist/session-remote.js +10 -2
  68. package/package.json +1 -1
@@ -0,0 +1,161 @@
1
+ import { ABORTED_CODE } from "../../agent.js";
2
+ export function inProcessLease() {
3
+ const busy = new Set();
4
+ return {
5
+ tryAcquire(session) {
6
+ if (busy.has(session))
7
+ return null;
8
+ busy.add(session);
9
+ let released = false;
10
+ return () => {
11
+ if (released)
12
+ return;
13
+ released = true;
14
+ busy.delete(session);
15
+ };
16
+ },
17
+ };
18
+ }
19
+ /** Clearly-transient network error codes (Node/undici), decisive on their own. */
20
+ const RETRYABLE_CODES = new Set([
21
+ "ECONNRESET",
22
+ "ETIMEDOUT",
23
+ "ENETUNREACH",
24
+ "ENETDOWN",
25
+ "EAI_AGAIN",
26
+ "EPIPE",
27
+ "UND_ERR_CONNECT_TIMEOUT",
28
+ "UND_ERR_SOCKET",
29
+ ]);
30
+ /** 429 (rate limit) and 5xx (server) are worth retrying; other statuses are decisive NON-retryable. */
31
+ const statusIsRetryable = (status) => status === 429 || (status >= 500 && status < 600);
32
+ /** Last-resort prose match, used only when no structured status/code is available. */
33
+ const RETRYABLE_MESSAGE = /\b(429|5\d\d|timeout|timed out|rate.?limit|overloaded|ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|socket hang up)\b/i;
34
+ /** A structured status/code decision, or `null` when the signal is absent/undecisive → fall to prose. */
35
+ function retryableFromSignal(signal) {
36
+ if (typeof signal.status === "number")
37
+ return statusIsRetryable(signal.status);
38
+ const { code } = signal;
39
+ if (typeof code === "number")
40
+ return statusIsRetryable(code);
41
+ if (typeof code === "string") {
42
+ if (RETRYABLE_CODES.has(code))
43
+ return true;
44
+ if (/^\d{3}$/.test(code))
45
+ return statusIsRetryable(Number(code)); // a status carried as a string
46
+ }
47
+ return null; // no code, or an unknown one — not decisive on its own
48
+ }
49
+ /** Classify `retryable`: structured status/code first, message prose only as the last-resort ceiling. */
50
+ export function classifyRetryable(details, signal) {
51
+ return retryableFromSignal(signal) ?? RETRYABLE_MESSAGE.test(details);
52
+ }
53
+ /** Pull a structured status/code off a thrown error (HTTP status or a network code, incl. its cause). */
54
+ function errorSignal(error) {
55
+ if (!error || typeof error !== "object")
56
+ return {};
57
+ const e = error;
58
+ const status = typeof e.status === "number" ? e.status : typeof e.statusCode === "number" ? e.statusCode : undefined;
59
+ const causeCode = e.cause && typeof e.cause === "object" ? e.cause.code : undefined;
60
+ return { status, code: e.code ?? causeCode };
61
+ }
62
+ /**
63
+ * Pull the structured error `code` pi records on a failed message's diagnostics. `diagnostics`
64
+ * accumulates across attempts (`appendAssistantMessageDiagnostic`), so the terminal cause is the LAST
65
+ * code-bearing entry — `findLast`, not `find`: an earlier attempt's transient 503 must not classify a
66
+ * terminal 400/auth failure as retryable. (Reverse scan rather than `findLast` — the tsconfig lib is
67
+ * ES2022.)
68
+ */
69
+ function messageSignal(message) {
70
+ const diagnostics = message.diagnostics ?? [];
71
+ for (let i = diagnostics.length - 1; i >= 0; i--) {
72
+ const code = diagnostics[i]?.error?.code;
73
+ if (code !== undefined)
74
+ return { code };
75
+ }
76
+ return {};
77
+ }
78
+ /**
79
+ * Terminal mapping, decided by the resolved message's stopReason: pi's prompt() resolves a message
80
+ * with stopReason "error"/"aborted" rather than throwing, so relying on catch alone would miss this
81
+ * entire failure class (violating SPEC MUST 1).
82
+ */
83
+ export function toTerminal(message) {
84
+ if (message.stopReason === "aborted") {
85
+ // A deliberate stop (control-plane abort / harness abort), not an error — see {@link ABORTED_CODE}
86
+ // for the consumer contract (design §6).
87
+ const details = message.errorMessage ?? "run aborted";
88
+ return { type: "failed", details, retryable: false, code: ABORTED_CODE };
89
+ }
90
+ if (message.stopReason === "error") {
91
+ const details = message.errorMessage ?? `model stopped: ${message.stopReason}`;
92
+ return { type: "failed", details, retryable: classifyRetryable(details, messageSignal(message)) };
93
+ }
94
+ return { type: "completed" };
95
+ }
96
+ export function errorToTerminal(error) {
97
+ const details = error instanceof Error ? error.message : String(error);
98
+ return { type: "failed", details, retryable: classifyRetryable(details, errorSignal(error)) };
99
+ }
100
+ /**
101
+ * Map prompt images to pi ImageContent, resizing each to model-friendly dimensions/size with pi's
102
+ * Photon resizer (reused from pi-coding-agent, lazy-imported so the common no-image headless path never
103
+ * loads the TUI module graph). A null resize (unresizable / Photon unavailable) keeps the original
104
+ * bytes — the provider then applies its own limit.
105
+ */
106
+ export async function toPiPromptOptions(prompt) {
107
+ if (!prompt.images || prompt.images.length === 0)
108
+ return undefined;
109
+ const { resizeImage } = await import("@earendil-works/pi-coding-agent");
110
+ const images = await Promise.all(prompt.images.map(async (img) => {
111
+ const resized = await resizeImage(Buffer.from(img.data, "base64"), img.mimeType, {
112
+ maxWidth: 1568,
113
+ maxHeight: 1568,
114
+ maxBytes: 5 * 1024 * 1024,
115
+ }).catch(() => null);
116
+ return resized
117
+ ? { type: "image", data: resized.data, mimeType: resized.mimeType }
118
+ : { type: "image", data: img.data, mimeType: img.mimeType };
119
+ }));
120
+ return { images };
121
+ }
122
+ // ── EventQueue: push→pull plumbing for a two-port engine ────────────────────
123
+ //
124
+ // Single-consumer async queue; single-threaded JS means no await interleaves between push and
125
+ // drain, so no locking. Engines that are natively async-iterable would not need it.
126
+ export class EventQueue {
127
+ buffer = [];
128
+ wake;
129
+ push(item) {
130
+ this.buffer.push(item);
131
+ const wake = this.wake;
132
+ this.wake = undefined;
133
+ wake?.();
134
+ }
135
+ /**
136
+ * Yield pushed events in order until `done` settles AND the buffer is drained. The terminal is
137
+ * produced separately (toTerminal); rejections of `done` are swallowed here (the caller awaits
138
+ * `run` itself) to avoid unhandled rejections.
139
+ */
140
+ async *drainUntil(done) {
141
+ let settled = false;
142
+ const onSettle = () => {
143
+ settled = true;
144
+ const wake = this.wake;
145
+ this.wake = undefined;
146
+ wake?.();
147
+ };
148
+ const finished = done.then(onSettle, onSettle);
149
+ while (true) {
150
+ while (this.buffer.length > 0) {
151
+ yield this.buffer.shift();
152
+ }
153
+ if (settled)
154
+ break;
155
+ await new Promise((resolve) => {
156
+ this.wake = resolve;
157
+ });
158
+ }
159
+ await finished;
160
+ }
161
+ }
package/dist/paths.d.ts CHANGED
@@ -24,6 +24,12 @@ export declare const STATE_DIRNAME = ".state";
24
24
  /** The config filenames, in load precedence. ONE source: the loader (below) and `scaffoldAgent`'s
25
25
  * already-an-agent refusal both read this, so "is there a config?" can't diverge between them. */
26
26
  export declare const AGENT_CONFIG_NAMES: readonly ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
27
+ /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). Definition
28
+ * data, not machinery: it declares WHICH endpoint the agent talks to, so it belongs beside the config
29
+ * and travels into the deployed image. The name lives HERE, with the other placement facts, because
30
+ * two neutral readers need it — the loader in engines/pi/models.ts and `dev`'s watcher, whose restart
31
+ * scope must not silently drift from what the worker actually loads. */
32
+ export declare const AGENT_MODELS_FILE = "models.json";
27
33
  export interface ResolvedPlacement {
28
34
  /** The AGENT directory — where the definition (persona.md/skills/tools/channels/schedules), the
29
35
  * config, and the machinery dirs (`.secrets/`, `.state/`) live. Absolute. */
package/dist/paths.js CHANGED
@@ -46,6 +46,12 @@ export const STATE_DIRNAME = ".state";
46
46
  /** The config filenames, in load precedence. ONE source: the loader (below) and `scaffoldAgent`'s
47
47
  * already-an-agent refusal both read this, so "is there a config?" can't diverge between them. */
48
48
  export const AGENT_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
49
+ /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). Definition
50
+ * data, not machinery: it declares WHICH endpoint the agent talks to, so it belongs beside the config
51
+ * and travels into the deployed image. The name lives HERE, with the other placement facts, because
52
+ * two neutral readers need it — the loader in engines/pi/models.ts and `dev`'s watcher, whose restart
53
+ * scope must not silently drift from what the worker actually loads. */
54
+ export const AGENT_MODELS_FILE = "models.json";
49
55
  /** The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded
50
56
  * inside ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
51
57
  * NOT evidence of an agent: `tools/` and `skills/` are ordinary names half the world's repositories
package/dist/pi.d.ts CHANGED
@@ -7,10 +7,11 @@ export { loadChannels, type ChannelCollision } from "./engines/pi/channel.ts";
7
7
  export { createPiAgentFromDir, type CreatePiAgentFromDirOptions, } from "./engines/pi/open.ts";
8
8
  export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
9
9
  export { defineConfig, listModels, resolveModel, type FastagentConfig } from "./engines/pi/config.ts";
10
- export { inProcessLease, type Lease, type Release, type SessionObserver } from "./engines/pi/invoke.ts";
10
+ export type { SessionObserver } from "./engines/pi/invoke.ts";
11
+ export { inProcessLease, type Lease, type Release } from "./engines/pi/turn-kit.ts";
11
12
  export { createPiSessionControl, type CreatePiSessionControlOptions, } from "./engines/pi/session-control.ts";
12
13
  export type { AnyModel } from "./engines/pi/harness.ts";
13
- export { inMemorySessionStore, jsonlSessionStore, type PiSessionReader, type PiSessionStore, } from "./engines/pi/sessions.ts";
14
+ export { inMemorySessionStore, jsonlSessionStore, type PiSessionReader, type PiSessionStore, type SessionInheritance, } from "./engines/pi/sessions.ts";
14
15
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore, type FastagentAuthOptions } from "./engines/pi/auth.ts";
15
16
  export { createPiModels, probeAuthSource, type CreatePiModelsOptions } from "./engines/pi/models.ts";
16
17
  export type { Models } from "@earendil-works/pi-ai";
package/dist/pi.js CHANGED
@@ -5,7 +5,7 @@ export { z } from "zod";
5
5
  export { loadChannels } from "./engines/pi/channel.js";
6
6
  export { createPiAgentFromDir, } from "./engines/pi/open.js";
7
7
  export { defineConfig, listModels, resolveModel } from "./engines/pi/config.js";
8
- export { inProcessLease } from "./engines/pi/invoke.js";
8
+ export { inProcessLease } from "./engines/pi/turn-kit.js";
9
9
  export { createPiSessionControl, } from "./engines/pi/session-control.js";
10
10
  export { inMemorySessionStore, jsonlSessionStore, } from "./engines/pi/sessions.js";
11
11
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore } from "./engines/pi/auth.js";
@@ -5,6 +5,8 @@
5
5
  // No model is preset: `fastagent dev` shows the full model catalog (models you already have
6
6
  // credentials for come first; picking one that needs auth logs you in inline) and writes your choice
7
7
  // below. Or set it by hand to a "provider/modelId" (`fastagent models` lists them).
8
+ // Self-hosted model (vLLM/Ollama/…) or your own gateway? Declare it in a models.json next to this
9
+ // file and select it like any other spec — see docs/configuration.md "Custom model endpoints".
8
10
  export default {
9
11
  // model: "openai-codex/gpt-5.5",
10
12
  // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
@@ -202,7 +202,8 @@ export function connectAgent(options) {
202
202
  // (carry it or reject it), never vanish on the wire while the client believes it was sent.
203
203
  const _invokeDriftGuard = {};
204
204
  void _invokeDriftGuard;
205
- // Same guard for Scope: the body carries session only — a new Scope field must force a decision.
205
+ // Same guard for Scope: the body carries session + the lineage extension — a new Scope field must
206
+ // force a decision (carry it or reject it), never vanish on the wire.
206
207
  const _scopeDriftGuard = {};
207
208
  void _scopeDriftGuard;
208
209
  return {
@@ -229,7 +230,14 @@ export function connectAgent(options) {
229
230
  const res = await fetchFn(`${base}/control/invoke`, {
230
231
  method: "POST",
231
232
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
232
- body: JSON.stringify({ session: scope.session, text: prompt.text }),
233
+ body: JSON.stringify({
234
+ session: scope.session,
235
+ text: prompt.text,
236
+ // Lineage rides the wire so a remote thread scope inherits server-side; the server
237
+ // reads it on the session-create path only, same as in-process.
238
+ ...(scope.parentSession !== undefined ? { parentSession: scope.parentSession } : {}),
239
+ ...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
240
+ }),
233
241
  signal: abort.signal,
234
242
  });
235
243
  watchdog.disarm(); // headers arrived
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on GitHub, Telegram, Slack, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",