@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.
- package/dist/agent.d.ts +11 -0
- package/dist/channels/agentcore.d.ts +16 -2
- package/dist/channels/agentcore.js +66 -4
- package/dist/channels/feishu/card.d.ts +20 -9
- package/dist/channels/feishu/card.js +27 -13
- package/dist/channels/feishu/feishu-api.d.ts +13 -2
- package/dist/channels/feishu/feishu.js +121 -17
- package/dist/channels/feishu/invoke-turn.d.ts +12 -2
- package/dist/channels/feishu/invoke-turn.js +159 -14
- package/dist/channels/feishu/normalize.js +97 -32
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/feishu/preview.d.ts +4 -3
- package/dist/channels/feishu/preview.js +77 -23
- package/dist/channels/feishu/scaffold/feishu-send.ts +9 -6
- package/dist/channels/http.js +15 -2
- package/dist/channels/invoke-turn-kit.d.ts +5 -2
- package/dist/channels/invoke-turn-kit.js +6 -2
- package/dist/channels/lark/scaffold/lark-send.ts +9 -6
- package/dist/channels/slack/invoke-turn.js +1 -1
- package/dist/channels/slack/slack.js +1 -5
- package/dist/channels/state.d.ts +0 -10
- package/dist/channels/state.js +2 -19
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/thread-participants.d.ts +7 -0
- package/dist/channels/thread-participants.js +3 -0
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +21 -1
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/start.js +40 -6
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +2 -2
- package/dist/cli/shared.d.ts +4 -2
- package/dist/cli/shared.js +12 -5
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/deploy/agentcore/plan.js +17 -0
- package/dist/deploy/agentcore/run.d.ts +7 -1
- package/dist/deploy/agentcore/run.js +93 -8
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +55 -10
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +8 -2
- package/dist/engines/pi/create.d.ts +2 -1
- package/dist/engines/pi/create.js +12 -7
- package/dist/engines/pi/harness.d.ts +6 -3
- package/dist/engines/pi/harness.js +4 -3
- package/dist/engines/pi/invoke-session.d.ts +32 -0
- package/dist/engines/pi/invoke-session.js +171 -0
- package/dist/engines/pi/invoke.d.ts +6 -27
- package/dist/engines/pi/invoke.js +49 -208
- package/dist/engines/pi/models.d.ts +45 -11
- package/dist/engines/pi/models.js +55 -8
- package/dist/engines/pi/session-builder.js +4 -2
- package/dist/engines/pi/session-control.d.ts +2 -1
- package/dist/engines/pi/sessions.d.ts +17 -1
- package/dist/engines/pi/sessions.js +292 -10
- package/dist/engines/pi/turn-kit.d.ts +56 -0
- package/dist/engines/pi/turn-kit.js +161 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +6 -0
- package/dist/pi.d.ts +3 -2
- package/dist/pi.js +1 -1
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/session-remote.js +10 -2
- 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
|
|
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/
|
|
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)
|
package/dist/session-remote.js
CHANGED
|
@@ -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
|
|
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({
|
|
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.
|
|
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",
|