@fastagent-sh/fastagent 0.17.1 → 0.19.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/README.md +25 -7
- package/dist/agent.d.ts +12 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.d.ts +4 -2
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +45 -15
- package/dist/channels/feishu/invoke-turn.d.ts +9 -3
- package/dist/channels/feishu/invoke-turn.js +152 -34
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +16 -111
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/state.d.ts +6 -0
- package/dist/channels/{state.js → kit/state.js} +4 -24
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +9 -6
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -12
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +2 -2
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +26 -14
- package/dist/cli/commands/fire.js +2 -2
- package/dist/cli/commands/info.js +28 -5
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +64 -95
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +14 -7
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +22 -4
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +16 -4
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -42
- package/dist/engines/pi/create.js +150 -75
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +35 -0
- package/dist/engines/pi/invoke-session.js +357 -0
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +59 -11
- package/dist/engines/pi/models.js +64 -9
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +88 -90
- package/dist/engines/pi/session-control.d.ts +11 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
- package/dist/engines/pi/turn-kit.js +188 -0
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +16 -0
- package/dist/pi.d.ts +15 -8
- package/dist/pi.js +7 -6
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session-remote.js +10 -2
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/channels/state.d.ts +0 -16
- package/dist/engines/pi/harness.d.ts +0 -112
- package/dist/engines/pi/harness.js +0 -153
- package/dist/engines/pi/invoke.js +0 -618
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -44
- package/dist/engines/pi/sessions.js +0 -182
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* (bad skill files, name collisions) are returned as data. An unreadable ② context file only warns (pi).
|
|
15
15
|
*/
|
|
16
16
|
import { realpathSync } from "node:fs";
|
|
17
|
-
import {
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
18
|
import { loadSkills } from "@earendil-works/pi-agent-core";
|
|
19
19
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
20
20
|
import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { log } from "../../log.js";
|
|
21
22
|
import { assertInsideAgentDir } from "../../paths.js";
|
|
22
23
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
23
24
|
export async function loadAgentDefinition(agentDir, options = {}) {
|
|
@@ -45,6 +46,119 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
45
46
|
const { skills, diagnostics, collisions } = await readSkills(e, root);
|
|
46
47
|
return { contextFiles, persona, skills, diagnostics, collisions, dir: root };
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Extension entry-point FILES under `<agentDir>/extensions/`, empty when there are none.
|
|
51
|
+
*
|
|
52
|
+
* Paths, not loaded objects — unlike skills (data: content inline, serializable), an extension is
|
|
53
|
+
* CODE that pi loads with jiti and binds to its own eventBus/runtime, so loading it here would
|
|
54
|
+
* reimplement pi's loader. The engine binding hands these to pi as `additionalExtensionPaths`, which
|
|
55
|
+
* survives `noExtensions: true` — that flag suppresses MACHINE-GLOBAL discovery (`~/.pi`), and the
|
|
56
|
+
* definition's own extensions were only ever collateral to it. FILES, not the directory: pi's paths
|
|
57
|
+
* are module specifiers, and a directory fails as `Cannot find module` into a
|
|
58
|
+
* `LoadExtensionsResult.errors` entry nothing reads.
|
|
59
|
+
*
|
|
60
|
+
* SEPARATE from {@link loadAgentDefinition} on purpose. Prompt and skills are re-read every invoke
|
|
61
|
+
* ("the directory is the agent, LIVE"); scanning for extension entry points is not, since the set
|
|
62
|
+
* cannot change without a restart. Called once per assembly, like `tools/`.
|
|
63
|
+
*
|
|
64
|
+
* Discovery runs on BOTH paths; loading does not. `chat` hands these to pi and runs them fully.
|
|
65
|
+
* Serving only announces them — pi's extension runtime is shared across sessions, and serving has
|
|
66
|
+
* concurrent turns for unrelated conversations (see `PiAgentSessionFactoryOptions.extensionPaths`).
|
|
67
|
+
* The refusals below are therefore about what the ARTIFACT may contain, and hold for both.
|
|
68
|
+
*
|
|
69
|
+
* Discovery follows pi's own rules, so an extension that works in pi works here:
|
|
70
|
+
*
|
|
71
|
+
* 1. a direct `*.ts` / `*.js` file;
|
|
72
|
+
* 2. a subdirectory with `index.ts` / `index.js`.
|
|
73
|
+
*
|
|
74
|
+
* pi has a third rule — a subdirectory whose `package.json` declares a `pi` field — which is NOT
|
|
75
|
+
* implemented here. That shape is reported rather than skipped: a definition whose extension silently
|
|
76
|
+
* fails to load is the failure mode this whole path exists to remove.
|
|
77
|
+
*
|
|
78
|
+
* Containment matches skills/tools/channels/schedules (the fifth of five surfaces): a symlinked
|
|
79
|
+
* `extensions/` escaping the agent dir is refused, and — like `loadModuleDir`, whose `entry.isFile()`
|
|
80
|
+
* excludes them — a symlinked ENTRY is not loaded either. pi's own discovery does follow those, but
|
|
81
|
+
* an extension reached through a link out of the definition is code the artifact does not carry: it
|
|
82
|
+
* resolves on the authoring machine and is missing in the container. Refused loudly, never silently.
|
|
83
|
+
*
|
|
84
|
+
* Absent is normal and silent; a FILE at that path is not — that is an author who meant something.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadExtensionPaths(agentDir, options = {}) {
|
|
87
|
+
const cwd = options.cwd ?? agentDir;
|
|
88
|
+
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
89
|
+
const rootResult = await e.absolutePath(agentDir);
|
|
90
|
+
if (!rootResult.ok)
|
|
91
|
+
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
92
|
+
const root = rootResult.value;
|
|
93
|
+
await assertInsideAgentDir(root, "extensions");
|
|
94
|
+
const dir = join(root, "extensions");
|
|
95
|
+
const listed = await e.listDir(dir);
|
|
96
|
+
if (!listed.ok) {
|
|
97
|
+
if (listed.error.code === "not_found")
|
|
98
|
+
return [];
|
|
99
|
+
throw new Error(`cannot read ${dir}: ${listed.error.message}`);
|
|
100
|
+
}
|
|
101
|
+
const paths = [];
|
|
102
|
+
for (const entry of listed.value) {
|
|
103
|
+
if (entry.kind === "symlink") {
|
|
104
|
+
// EVERY symlink here is announced, without guessing whether it meant to be an extension. The
|
|
105
|
+
// two mistakes are not equal: a needless line about a symlinked README costs a glance, while
|
|
106
|
+
// staying quiet about a symlinked extension loses a feature silently and only shows up in the
|
|
107
|
+
// container. A name-based guess also cannot see through the link — `audit.ext -> some/dir` is
|
|
108
|
+
// a directory candidate to pi and a mystery here — so the warning says what it knows and
|
|
109
|
+
// tells the author when to ignore it.
|
|
110
|
+
warnSymlinkRefused(entry.path);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
|
|
114
|
+
if (entry.kind === "file")
|
|
115
|
+
paths.push(entry.path);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (entry.kind === "file")
|
|
119
|
+
continue; // a README, a .json — not an extension, not a problem
|
|
120
|
+
const index = await firstRealFile(e, [join(entry.path, "index.ts"), join(entry.path, "index.js")]);
|
|
121
|
+
if (index.path) {
|
|
122
|
+
paths.push(index.path);
|
|
123
|
+
}
|
|
124
|
+
else if (!index.refused) {
|
|
125
|
+
// Silent when the index WAS found and refused for being a symlink: that warning already named
|
|
126
|
+
// the real problem, and "expected index.ts" on top of it describes a directory that has one.
|
|
127
|
+
log.warn(`[fastagent] ${entry.path} is not a loadable extension: expected index.ts or index.js ` +
|
|
128
|
+
`(pi's package.json "pi" manifest form is not supported here) — it will not be loaded`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return paths.sort();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The first candidate that is a REAL file. `exists` would follow a symlink, which is how a
|
|
135
|
+
* subdirectory's `index.ts` could otherwise point outside the definition and slip past the rule the
|
|
136
|
+
* top-level entries already follow.
|
|
137
|
+
*/
|
|
138
|
+
/** The first real file among the candidates, and whether one was found but REFUSED as a symlink. */
|
|
139
|
+
async function firstRealFile(e, candidates) {
|
|
140
|
+
let refused = false;
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
const info = await e.fileInfo(candidate);
|
|
143
|
+
if (!info.ok) {
|
|
144
|
+
if (info.error.code === "not_found")
|
|
145
|
+
continue;
|
|
146
|
+
throw new Error(`cannot read ${candidate}: ${info.error.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (info.value.kind === "file")
|
|
149
|
+
return { path: candidate, refused };
|
|
150
|
+
if (info.value.kind === "symlink") {
|
|
151
|
+
warnSymlinkRefused(candidate);
|
|
152
|
+
refused = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { refused };
|
|
156
|
+
}
|
|
157
|
+
function warnSymlinkRefused(path) {
|
|
158
|
+
log.warn(`[fastagent] ${path} is a symlink and will not be loaded: an extension must be a real file inside ` +
|
|
159
|
+
`the definition so it travels with the artifact — move it in. (If it is not an extension, ` +
|
|
160
|
+
`keep it outside extensions/ to silence this.)`);
|
|
161
|
+
}
|
|
48
162
|
/** The skills half, shared by the full load and {@link loadAgentSkills}. `root` is already resolved. */
|
|
49
163
|
async function readSkills(e, root) {
|
|
50
164
|
// Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
|
|
@@ -81,16 +195,6 @@ export async function loadAgentSkills(agentDir, options = {}) {
|
|
|
81
195
|
// (the findings memo) on it, and "./agent" vs an absolute path must not become two definitions.
|
|
82
196
|
return { ...(await readSkills(e, rootResult.value)), dir: rootResult.value };
|
|
83
197
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
86
|
-
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
87
|
-
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
88
|
-
* does not act on where a user's paths point.
|
|
89
|
-
*/
|
|
90
|
-
export function isUnderDir(targetPath, baseDir) {
|
|
91
|
-
const rel = relative(baseDir, targetPath);
|
|
92
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
93
|
-
}
|
|
94
198
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
|
|
95
199
|
* A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
|
|
96
200
|
export function canonicalPath(p) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE L0: pi's `AgentSession`, one per invoke, over the same durable record —
|
|
3
|
+
* [conformance-levels.md](../../../docs/design/conformance-levels.md) §2's `per-invoke` posture.
|
|
4
|
+
* Build a session, run one turn, dispose; continuity lives in the record, never in this process.
|
|
5
|
+
*
|
|
6
|
+
* Why this class: pi 0.84 replaced `AgentHarness` with an unimplemented lane-based skeleton, and pi
|
|
7
|
+
* does not consume that class itself — its TUI, RPC and SDK all run on `AgentSession`. Being the
|
|
8
|
+
* sole consumer of a surface nobody dogfoods is a position, not an architecture.
|
|
9
|
+
*
|
|
10
|
+
* Events are translated ONCE, into the rich `SessionEvent` vocabulary the observation plane speaks;
|
|
11
|
+
* the SPEC stream is a projection of that (`docs/design/session-control.md` §6 — one translation
|
|
12
|
+
* plus one projection, never two parallel ones).
|
|
13
|
+
*
|
|
14
|
+
* Two disciplines this file exists to hold:
|
|
15
|
+
* - the turn's outcome comes from the EVENT STREAM, never from an index into session state, which
|
|
16
|
+
* compaction and overflow recovery both rewrite mid-turn;
|
|
17
|
+
* - `run_started` is published BEFORE the session is bound, so a dispatch racing the build queues on
|
|
18
|
+
* the run's controls instead of finding no run.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import type { SessionInheritance } from "./session-inheritance.ts";
|
|
22
|
+
import { type Agent } from "../../agent.ts";
|
|
23
|
+
import { type Lease, type SessionObserver } from "./turn-kit.ts";
|
|
24
|
+
/** Open-or-create the session behind `sessionId` and bind an `AgentSession` to it, per invoke.
|
|
25
|
+
* `inherit` reaches the CREATE path only — an existing session ignores it. */
|
|
26
|
+
export type PiAgentSessionFactory = (sessionId: string, inherit?: SessionInheritance) => Promise<AgentSession>;
|
|
27
|
+
export interface CreatePiAgentFromSessionOptions {
|
|
28
|
+
sessionFactory: PiAgentSessionFactory;
|
|
29
|
+
/** Single-writer lease. Defaults to the in-process per-session fail-fast lease. */
|
|
30
|
+
lease?: Lease;
|
|
31
|
+
/** Observation-plane tap: every rich event of every run, plus the run's live {@link RunControls}
|
|
32
|
+
* on `run_started`. Optional; the SPEC stream is identical with or without it. */
|
|
33
|
+
observer?: SessionObserver;
|
|
34
|
+
}
|
|
35
|
+
export declare function createPiAgentFromSession(options: CreatePiAgentFromSessionOptions): Agent;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { ABORTED_CODE, SESSION_BUSY_CODE, } from "../../agent.js";
|
|
2
|
+
import { cancellableStream } from "../../collect.js";
|
|
3
|
+
import { log } from "../../log.js";
|
|
4
|
+
import { EventQueue, errorToTerminal, inProcessLease, projectAgentEvent, toPiPromptOptions, toTerminal, } from "./turn-kit.js";
|
|
5
|
+
/**
|
|
6
|
+
* pi session events into the rich `SessionEvent` vocabulary — the SINGLE translation point. Events
|
|
7
|
+
* with no vocabulary yet (agent_start, turn_start, entry_appended, …) are dropped.
|
|
8
|
+
*
|
|
9
|
+
* `auto_retry_start` reports as a `retry_scheduled` with `operation: "assistant"`: pi retries a
|
|
10
|
+
* failed ANSWER request itself, which the summarization-only cases of that vocabulary predate.
|
|
11
|
+
*/
|
|
12
|
+
function toSessionEvent(event, runId) {
|
|
13
|
+
const at = Date.now();
|
|
14
|
+
switch (event.type) {
|
|
15
|
+
case "message_start":
|
|
16
|
+
// Assistant streaming only — a user/toolResult message is not a live message boundary.
|
|
17
|
+
if (event.message.role !== "assistant")
|
|
18
|
+
return null;
|
|
19
|
+
return { type: "message_started", timestamp: at, runId, data: {} };
|
|
20
|
+
case "message_update": {
|
|
21
|
+
// An empty delta is not output: it moves no consumer's state, and treating it as output would
|
|
22
|
+
// spend the silent window that auto-retry is allowed to use (see runOnSession).
|
|
23
|
+
const ev = event.assistantMessageEvent;
|
|
24
|
+
if (ev.type === "text_delta") {
|
|
25
|
+
return ev.delta === ""
|
|
26
|
+
? null
|
|
27
|
+
: { type: "message_delta", timestamp: at, runId, data: { channel: "text", delta: ev.delta } };
|
|
28
|
+
}
|
|
29
|
+
if (ev.type === "thinking_delta") {
|
|
30
|
+
return ev.delta === ""
|
|
31
|
+
? null
|
|
32
|
+
: { type: "message_delta", timestamp: at, runId, data: { channel: "thinking", delta: ev.delta } };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
case "message_end":
|
|
37
|
+
if (event.message.role !== "assistant")
|
|
38
|
+
return null;
|
|
39
|
+
return { type: "message_finished", timestamp: at, runId, data: {} };
|
|
40
|
+
case "tool_execution_start":
|
|
41
|
+
return {
|
|
42
|
+
type: "tool_started",
|
|
43
|
+
timestamp: at,
|
|
44
|
+
runId,
|
|
45
|
+
data: { id: event.toolCallId, name: event.toolName, args: event.args },
|
|
46
|
+
};
|
|
47
|
+
case "tool_execution_update":
|
|
48
|
+
return {
|
|
49
|
+
type: "tool_progress",
|
|
50
|
+
timestamp: at,
|
|
51
|
+
runId,
|
|
52
|
+
data: { id: event.toolCallId, name: event.toolName, partialResult: event.partialResult },
|
|
53
|
+
};
|
|
54
|
+
case "tool_execution_end":
|
|
55
|
+
return {
|
|
56
|
+
type: "tool_finished",
|
|
57
|
+
timestamp: at,
|
|
58
|
+
runId,
|
|
59
|
+
data: { id: event.toolCallId, isError: event.isError, content: event.result },
|
|
60
|
+
};
|
|
61
|
+
case "queue_update":
|
|
62
|
+
return {
|
|
63
|
+
type: "queue_changed",
|
|
64
|
+
timestamp: at,
|
|
65
|
+
runId,
|
|
66
|
+
data: { steering: event.steering.length, followUp: event.followUp.length },
|
|
67
|
+
};
|
|
68
|
+
case "auto_retry_start": {
|
|
69
|
+
const retry = {
|
|
70
|
+
type: "retry_scheduled",
|
|
71
|
+
timestamp: at,
|
|
72
|
+
runId,
|
|
73
|
+
data: {
|
|
74
|
+
operation: "assistant",
|
|
75
|
+
attempt: event.attempt,
|
|
76
|
+
maxAttempts: event.maxAttempts,
|
|
77
|
+
delayMs: event.delayMs,
|
|
78
|
+
error: event.errorMessage,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return retry;
|
|
82
|
+
}
|
|
83
|
+
case "summarization_retry_scheduled": {
|
|
84
|
+
const retry = {
|
|
85
|
+
type: "retry_scheduled",
|
|
86
|
+
timestamp: at,
|
|
87
|
+
runId,
|
|
88
|
+
data: {
|
|
89
|
+
operation: "compaction",
|
|
90
|
+
attempt: event.attempt,
|
|
91
|
+
maxAttempts: event.maxAttempts,
|
|
92
|
+
delayMs: event.delayMs,
|
|
93
|
+
error: event.errorMessage,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return retry;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The turn's outcome. `prompt()` resolves void and never throws for an engine-side failure (measured:
|
|
104
|
+
* a provider error and an abort both resolve normally), so the terminal comes from the assistant
|
|
105
|
+
* message the run ended on.
|
|
106
|
+
*
|
|
107
|
+
* That message is taken from the EVENT STREAM, not from `session.state.messages`. Session state is
|
|
108
|
+
* mutable mid-turn: compaction replaces the array, and overflow recovery splices the last assistant
|
|
109
|
+
* message out of it outright (`state.messages = messages.slice(0, -1)`). Any index into it is a
|
|
110
|
+
* turn boundary that the engine is free to invalidate, while a `message_end` payload is a fact that
|
|
111
|
+
* already happened. Auto-compaction runs its own model call outside the agent's event stream, so it
|
|
112
|
+
* cannot masquerade as the turn's answer here.
|
|
113
|
+
*/
|
|
114
|
+
const ENGINE_PRODUCED_NOTHING = {
|
|
115
|
+
// Unreachable on a settled run: pi ends every outcome, error and abort included, with an assistant
|
|
116
|
+
// message. Reaching it means the engine broke its own contract — name the engine, not the turn.
|
|
117
|
+
type: "failed",
|
|
118
|
+
details: "the engine settled the run without ending an assistant message",
|
|
119
|
+
retryable: false,
|
|
120
|
+
};
|
|
121
|
+
export function createPiAgentFromSession(options) {
|
|
122
|
+
const { sessionFactory, lease = inProcessLease(), observer } = options;
|
|
123
|
+
async function* turn(scope, prompt, { onCancelReady, wasCancelled }) {
|
|
124
|
+
const release = lease.tryAcquire(scope.session);
|
|
125
|
+
if (!release) {
|
|
126
|
+
// Rejected BEFORE acceptance: no run exists, so the observer sees nothing (replay-safe).
|
|
127
|
+
yield {
|
|
128
|
+
type: "failed",
|
|
129
|
+
details: "session busy: a turn is already in flight for this session",
|
|
130
|
+
retryable: true,
|
|
131
|
+
code: SESSION_BUSY_CODE,
|
|
132
|
+
};
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// The run exists from here: exactly one run_started, exactly one run_settled. The settlement is
|
|
136
|
+
// emitted in the outer finally, immediately before the lease releases, so the observation
|
|
137
|
+
// plane's "running" window equals the lease window — state() must never read idle while a new
|
|
138
|
+
// invoke would still be rejected session_busy. A run with no recorded outcome was cancelled by
|
|
139
|
+
// the caller (SPEC: cancellation has no terminal event), which settles as aborted.
|
|
140
|
+
const runId = crypto.randomUUID();
|
|
141
|
+
let outcome;
|
|
142
|
+
const observe = (event, run) => {
|
|
143
|
+
if (!event || !observer)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
observer(scope.session, event, run);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// The observation plane must never break the data plane; a broken hub is its own problem.
|
|
150
|
+
log.warn(`[fastagent] session observer threw (event ${event.type}): ${String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
// run_started is published BEFORE the session is built, so no early event can outrun the
|
|
154
|
+
// registration — which means the controls have to await the build rather than reject during it:
|
|
155
|
+
// a dispatch that races it simply queues on the freshly bound session. A build failure rejects
|
|
156
|
+
// the gate, so a pending dispatch learns why instead of hanging.
|
|
157
|
+
let sessionReady;
|
|
158
|
+
let sessionFailed;
|
|
159
|
+
const bound = new Promise((resolve, reject) => {
|
|
160
|
+
sessionReady = resolve;
|
|
161
|
+
sessionFailed = reject;
|
|
162
|
+
});
|
|
163
|
+
bound.catch(() => { }); // observed through the controls only when a dispatch actually happens
|
|
164
|
+
// Stale-controls guard: after settlement pi's steer()/followUp()/abort() would still resolve
|
|
165
|
+
// (they queue onto a session about to be disposed), which is a silent acceptance of a command
|
|
166
|
+
// that can never take effect. The check and the engine call share one synchronous block — pi
|
|
167
|
+
// enqueues at method entry, so a check behind its own await would only shrink the race.
|
|
168
|
+
let settled = false;
|
|
169
|
+
const settledError = () => new Error("run already settled; the command cannot take effect");
|
|
170
|
+
// Aborted classification has two sources, either sufficient: pi's own stopReason "aborted", and
|
|
171
|
+
// control-plane INTENT — providers do not uniformly attribute an aborted stream, so an abort
|
|
172
|
+
// that was still in flight when the terminal arrived counts too.
|
|
173
|
+
let abortsInFlight = 0;
|
|
174
|
+
let abortSucceeded = false;
|
|
175
|
+
const controls = {
|
|
176
|
+
async steer(p) {
|
|
177
|
+
const opts = await toPiPromptOptions(p);
|
|
178
|
+
const session = await bound;
|
|
179
|
+
if (settled)
|
|
180
|
+
throw settledError();
|
|
181
|
+
await session.steer(p.text, opts?.images);
|
|
182
|
+
},
|
|
183
|
+
async followUp(p) {
|
|
184
|
+
const opts = await toPiPromptOptions(p);
|
|
185
|
+
const session = await bound;
|
|
186
|
+
if (settled)
|
|
187
|
+
throw settledError();
|
|
188
|
+
await session.followUp(p.text, opts?.images);
|
|
189
|
+
},
|
|
190
|
+
async abort() {
|
|
191
|
+
const session = await bound;
|
|
192
|
+
if (settled)
|
|
193
|
+
throw settledError();
|
|
194
|
+
abortsInFlight++;
|
|
195
|
+
try {
|
|
196
|
+
await session.abort();
|
|
197
|
+
abortSucceeded = true;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
abortsInFlight--;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
observe({ type: "run_started", timestamp: Date.now(), runId, data: {} }, controls);
|
|
205
|
+
try {
|
|
206
|
+
let session;
|
|
207
|
+
try {
|
|
208
|
+
// The scope's lineage reaches the store's CREATE path only — an existing session opens
|
|
209
|
+
// exactly as before, whatever the scope names.
|
|
210
|
+
session = await sessionFactory(scope.session, scope.parentSession === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: {
|
|
213
|
+
parentSession: scope.parentSession,
|
|
214
|
+
...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
|
|
215
|
+
});
|
|
216
|
+
sessionReady(session);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Setup failures (session open, auth, a broken definition) are EVENTS, never throws
|
|
220
|
+
// (MUST 2) — and they settle the run as failed: an unrecorded outcome means the caller
|
|
221
|
+
// cancelled, which this is not.
|
|
222
|
+
sessionFailed(error); // a pending dispatch learns the run cannot take commands
|
|
223
|
+
const terminal = errorToTerminal(error);
|
|
224
|
+
outcome = { status: "failed", error: { message: terminal.details, retryable: terminal.retryable } };
|
|
225
|
+
settled = true;
|
|
226
|
+
yield terminal;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const abort = () => session.abort().catch(() => { });
|
|
231
|
+
onCancelReady(() => void abort());
|
|
232
|
+
const queue = new EventQueue();
|
|
233
|
+
let finalAssistant;
|
|
234
|
+
/** Whether any of THIS attempt's answer has been streamed — the only output a retry duplicates. */
|
|
235
|
+
let streamedAnswer = false;
|
|
236
|
+
/** Set when a retry is refused because the answer already streamed — carries the ending error. */
|
|
237
|
+
let retriedAfterAnswer;
|
|
238
|
+
const unsub = session.subscribe((event) => {
|
|
239
|
+
if (retriedAfterAnswer !== undefined)
|
|
240
|
+
return; // decided; the retry's output is not ours
|
|
241
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
242
|
+
finalAssistant = event.message;
|
|
243
|
+
}
|
|
244
|
+
// pi retries a failed assistant request by DISCARDING that attempt's assistant message and
|
|
245
|
+
// asking again. Everything the turn achieved before it survives — executed tools keep
|
|
246
|
+
// their persisted results, and the retry resumes from them — so the only thing a retry can
|
|
247
|
+
// duplicate is answer text already streamed, which SPEC deltas cannot retract. Refuse it
|
|
248
|
+
// exactly there: refusing on tool events instead would push the retry out to the CALLER,
|
|
249
|
+
// who can only re-run the whole prompt and execute the tool a second time.
|
|
250
|
+
if (event.type === "auto_retry_start" && streamedAnswer) {
|
|
251
|
+
retriedAfterAnswer = event.errorMessage;
|
|
252
|
+
// Not synchronously: pi emits this event BEFORE creating the controller that makes its
|
|
253
|
+
// backoff abortable, so an abort from inside the listener would find nothing to cancel
|
|
254
|
+
// and the turn would still pay the delay and burn a provider call on a discarded answer.
|
|
255
|
+
queueMicrotask(() => void abort());
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const rich = toSessionEvent(event, runId);
|
|
259
|
+
if (!rich)
|
|
260
|
+
return;
|
|
261
|
+
observe(rich);
|
|
262
|
+
const projected = projectAgentEvent(rich);
|
|
263
|
+
if (!projected)
|
|
264
|
+
return;
|
|
265
|
+
if (projected.type === "text" || projected.type === "thinking")
|
|
266
|
+
streamedAnswer = true;
|
|
267
|
+
queue.push(projected);
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
// Resolving prompt options lazy-loads the image pipeline and re-encodes every attachment,
|
|
271
|
+
// so it both takes time and can throw before any engine work exists to fail. Hence the two
|
|
272
|
+
// guards, in this order and no earlier: its failure is a turn failure (MUST 2), and the
|
|
273
|
+
// latch has to be read after the LAST await before the call — the door armed above only
|
|
274
|
+
// stops a RUNNING session, so a consumer who walked away during the build or the resize
|
|
275
|
+
// would knock on an idle one and have the turn start anyway.
|
|
276
|
+
let promptOptions;
|
|
277
|
+
try {
|
|
278
|
+
promptOptions = await toPiPromptOptions(prompt);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const terminal = errorToTerminal(error);
|
|
282
|
+
outcome = { status: "failed", error: { message: terminal.details, retryable: terminal.retryable } };
|
|
283
|
+
settled = true;
|
|
284
|
+
yield terminal;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (wasCancelled()) {
|
|
288
|
+
settled = true;
|
|
289
|
+
await abort();
|
|
290
|
+
return; // cancelled: the outer finally settles it as aborted
|
|
291
|
+
}
|
|
292
|
+
const run = session.prompt(prompt.text, promptOptions);
|
|
293
|
+
yield* queue.drainUntil(run);
|
|
294
|
+
let terminal;
|
|
295
|
+
try {
|
|
296
|
+
await run;
|
|
297
|
+
terminal =
|
|
298
|
+
retriedAfterAnswer !== undefined
|
|
299
|
+
? { type: "failed", details: retriedAfterAnswer, retryable: true }
|
|
300
|
+
: finalAssistant
|
|
301
|
+
? toTerminal(finalAssistant)
|
|
302
|
+
: ENGINE_PRODUCED_NOTHING;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
terminal = errorToTerminal(error);
|
|
306
|
+
}
|
|
307
|
+
if ((abortSucceeded || abortsInFlight > 0) && terminal.type === "failed") {
|
|
308
|
+
terminal = { type: "failed", details: terminal.details, retryable: false, code: ABORTED_CODE };
|
|
309
|
+
}
|
|
310
|
+
if (terminal.type === "failed") {
|
|
311
|
+
outcome =
|
|
312
|
+
terminal.code === ABORTED_CODE
|
|
313
|
+
? // Carry the detail: an independent error that raced an accepted abort must stay
|
|
314
|
+
// diagnosable in the settlement, which is what audit consumers read.
|
|
315
|
+
{ status: "aborted", error: { message: terminal.details, retryable: false } }
|
|
316
|
+
: {
|
|
317
|
+
status: "failed",
|
|
318
|
+
error: { code: terminal.code, message: terminal.details, retryable: terminal.retryable },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
outcome = { status: "completed" };
|
|
323
|
+
}
|
|
324
|
+
// Commands become ineffective the moment the run resolved — not at the outer finally,
|
|
325
|
+
// which sits behind a consumer-paced `yield`.
|
|
326
|
+
settled = true;
|
|
327
|
+
yield terminal;
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
settled = true;
|
|
331
|
+
unsub();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
// NO session_shutdown here, deliberately. A per-invoke session makes one look right, but the
|
|
336
|
+
// extension INSTANCE it would tear down is not per-invoke: extensions belong to the agent's
|
|
337
|
+
// assembly and every turn shares one. Emitting a shutdown per turn had a finished
|
|
338
|
+
// turn clearing a timer a concurrent turn had just opened (measured, and pinned in
|
|
339
|
+
// definition-extensions.test.ts). The lifecycle has to match the instance, not the session
|
|
340
|
+
// wrapper: one agent, one instance, no per-turn teardown. Extensions that need per-turn
|
|
341
|
+
// cleanup do it in the tool or handler that opened the resource.
|
|
342
|
+
try {
|
|
343
|
+
session.dispose();
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
log.warn(`[fastagent] session dispose failed during cleanup: ${String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
settled = true;
|
|
352
|
+
observe({ type: "run_settled", timestamp: Date.now(), runId, data: outcome ?? { status: "aborted" } });
|
|
353
|
+
release(); // after the settlement, so the next invoke for this session cannot outrun it
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { invoke: (scope, prompt) => cancellableStream((hooks) => turn(scope, prompt, hooks)) };
|
|
357
|
+
}
|
package/dist/engines/pi/login.js
CHANGED
|
@@ -18,13 +18,16 @@ function anySignal(...signals) {
|
|
|
18
18
|
return present.length === 0 ? undefined : present.length === 1 ? present[0] : AbortSignal.any(present);
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Map pi-ai's `
|
|
22
|
-
* resolves, cancelling a prompt the provider left pending (a manual-code paste racing a
|
|
23
|
-
* server it just won) so the one-shot CLI exits instead of hanging on stdin.
|
|
21
|
+
* Map pi-ai's `ProviderAuthInteraction` onto the injected {@link LoginIO}. `doneSignal` fires when
|
|
22
|
+
* the flow resolves, cancelling a prompt the provider left pending (a manual-code paste racing a
|
|
23
|
+
* callback server it just won) so the one-shot CLI exits instead of hanging on stdin.
|
|
24
|
+
*
|
|
25
|
+
* The signal is REQUIRED by that type (pi normalizes it before calling a provider), so a caller that
|
|
26
|
+
* passes none gets one that never fires — the same "no cancellation" the optional field meant.
|
|
24
27
|
*/
|
|
25
28
|
function authCallbacks(io, userSignal, doneSignal) {
|
|
26
29
|
return {
|
|
27
|
-
signal: userSignal,
|
|
30
|
+
signal: userSignal ?? new AbortController().signal,
|
|
28
31
|
prompt: async (p) => {
|
|
29
32
|
if (p.type === "select") {
|
|
30
33
|
const v = await io.select(p.message, p.options.map((o) => ({ value: o.id, label: o.label, hint: o.description })));
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
* The pi `Models` collection — the single hub that owns BOTH model resolution (provider/modelId
|
|
3
|
-
* lookup) AND auth (per-request credential resolution). fastagent builds one per opener and threads
|
|
4
|
-
* it into the harness alongside the selected `model`; the two must come from the same collection so
|
|
5
|
-
* the model's provider auth is in scope.
|
|
6
|
-
*/
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
7
2
|
import { type Api, type Model, type Models, type Provider } from "@earendil-works/pi-ai";
|
|
8
3
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
9
4
|
import { type FastagentAuthOptions } from "./auth.ts";
|
|
10
5
|
import { type InteractiveLoginKind } from "./login.ts";
|
|
6
|
+
/** The DEFINITION-LOCAL custom-endpoint file, in pi's own models.json schema (see pi's docs/models.md):
|
|
7
|
+
* declare a self-hosted / gateway endpoint as `{ providers: { <id>: { baseUrl, api, apiKey, models } } }`
|
|
8
|
+
* and select it with a `<id>/<modelId>` model spec. Keys belong in the environment — `apiKey` supports
|
|
9
|
+
* `"$ENV_VAR"` interpolation and `"!command"` — with the NAME listed in `deploy.secrets` so the value
|
|
10
|
+
* travels to the host. Living in the agent dir is the whole point: it is part of the definition, so it
|
|
11
|
+
* is baked into the deployed image. pi's MACHINE-GLOBAL `~/.pi/agent/models.json` stays unread — that
|
|
12
|
+
* one is builder-machine state, and reading it would make an agent work locally and lose its model on
|
|
13
|
+
* deploy. The NAME itself lives in the neutral paths.ts — `dev`'s watcher needs the same one, and a
|
|
14
|
+
* second spelling would let the restart scope drift from what the worker loads. */
|
|
11
15
|
export interface CreatePiModelsOptions extends FastagentAuthOptions {
|
|
12
16
|
/** Credentials file path. Defaults to the global `~/.fastagent/.secrets/auth.json`; the directory opener passes
|
|
13
17
|
* the project-level `<root>/.secrets/auth.json`. */
|
|
@@ -23,18 +27,62 @@ export interface CreatePiModelsOptions extends FastagentAuthOptions {
|
|
|
23
27
|
* is upstream-owned).
|
|
24
28
|
*/
|
|
25
29
|
export declare function createPiModels(options?: CreatePiModelsOptions): Models;
|
|
30
|
+
/**
|
|
31
|
+
* pi's Model with the API-shape generic erased — fastagent only passes models through to the engine,
|
|
32
|
+
* so the generic carries no information. One alias keeps the `any` auditable.
|
|
33
|
+
*/
|
|
34
|
+
export type AnyModel = Model<any>;
|
|
35
|
+
/**
|
|
36
|
+
* The serving default for reasoning effort, pinned to what pi's TUI defaults to (its own
|
|
37
|
+
* DEFAULT_THINKING_LEVEL) — NOT inherited from the engine, whose fallback is "off": an author vibes
|
|
38
|
+
* at "medium" in pi and must get "medium" when served (fidelity), and pinning the value here means
|
|
39
|
+
* an upstream default change in either place cannot silently alter deployments. Models that do not
|
|
40
|
+
* support a level are clamped by pi per model.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_THINKING_LEVEL: ThinkingLevel;
|
|
26
43
|
/**
|
|
27
44
|
* The `ModelRuntime`-shaped sibling of {@link createPiModels} — the SAME hub semantics (built-in
|
|
28
45
|
* providers + fastagent's credential store at `authPath`) in the type pi's session services require
|
|
29
|
-
* (`createAgentSessionServices({ modelRuntime })`).
|
|
30
|
-
*
|
|
31
|
-
* surface equals serving's.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
46
|
+
* (`createAgentSessionServices({ modelRuntime })`). Built-ins PLUS the agent's own
|
|
47
|
+
* {@link AGENT_MODELS_FILE} when `agentDir` is given (a dir-less caller gets built-ins only), and no
|
|
48
|
+
* availability network, so the model surface equals serving's.
|
|
49
|
+
*
|
|
50
|
+
* `ModelRuntime` also takes `Provider` INSTANCES via `registerNativeProvider` (pi 0.83); the
|
|
51
|
+
* declarative file is what this rung wires because it is data that travels with the definition.
|
|
34
52
|
*/
|
|
35
53
|
export declare function createPiModelRuntime(options?: FastagentAuthOptions & {
|
|
36
54
|
authPath?: string;
|
|
55
|
+
/** The agent dir, whose {@link AGENT_MODELS_FILE} declares custom endpoints. Omit for built-ins only. */
|
|
56
|
+
agentDir?: string;
|
|
57
|
+
/** Where the dynamic model-catalog cache goes; defaults to the agent's resolved state root. */
|
|
58
|
+
stateRoot?: string;
|
|
59
|
+
/** Extra providers for the ids the built-ins do not cover — the CODE-shaped sibling of models.json,
|
|
60
|
+
* for what a file cannot express (minting a token per request, a test fake).
|
|
61
|
+
*
|
|
62
|
+
* On an id COLLISION the file wins, not this: upstream installs a native provider as the BASE and
|
|
63
|
+
* composes the models.json entry over it. Right way round — where a deployed agent's traffic goes
|
|
64
|
+
* is a property of the definition, not of the program that embedded it — but it does mean a same-id
|
|
65
|
+
* file entry silently replaces the endpoint injected here. Use a distinct id to keep both. */
|
|
66
|
+
providers?: Provider[];
|
|
37
67
|
}): Promise<ModelRuntime>;
|
|
68
|
+
/**
|
|
69
|
+
* How a model's credential will REACH a deployed agent — the question `deploy` asks, which
|
|
70
|
+
* {@link probeAuthSource} cannot answer: it flattens every models.json endpoint to the display label
|
|
71
|
+
* "configured API key", so a self-hosted endpoint looks credential-less to the deploy gate even when
|
|
72
|
+
* its key is sitting in an env var.
|
|
73
|
+
*
|
|
74
|
+
* - `envVar`: an environment variable backs it, BY NAME — the shape `deploy` already understands, so
|
|
75
|
+
* the value carries as a host secret with no extra declaration from the author.
|
|
76
|
+
* - `inDefinition`: the definition itself carries it (a literal `apiKey`, or a `!command` run on the
|
|
77
|
+
* host). Nothing for `deploy` to carry — and nothing to gate on either, which is the point: the
|
|
78
|
+
* `fastagent login` remedy is meaningless for a provider login cannot serve.
|
|
79
|
+
*
|
|
80
|
+
* Neither set = a stored credential or nothing at all; the existing auth.json / gate paths decide.
|
|
81
|
+
*/
|
|
82
|
+
export declare function modelCredentialCarry(runtime: ModelRuntime, spec: string): {
|
|
83
|
+
envVar?: string;
|
|
84
|
+
inDefinition: boolean;
|
|
85
|
+
};
|
|
38
86
|
/** Per-provider auth status for the first-run model picker: usable now (with the source label), not
|
|
39
87
|
* configured, or configured-but-broken (expired token, refresh failure, corrupt store — kept as DATA
|
|
40
88
|
* so the picker can show it instead of silently dropping the provider). Non-ready states carry the
|