@fastagent-sh/fastagent 0.14.0 → 0.16.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared definition-aware session builder: open a directory's assembled agent as a resident pi
|
|
3
|
+
* `AgentSessionRuntime`. Extracted from chat.ts (session-control Phase 0) as the proof of the
|
|
4
|
+
* assembly seam — independently instantiable, running the SAME agent that `dev`/`start` serve. The
|
|
5
|
+
* TUI (chat.ts) is its one consumer: the session control plane (Phases 1–3) was built on the invoke
|
|
6
|
+
* pipeline instead of this resident runtime, so control-plane observation covers invoke-driven runs
|
|
7
|
+
* and deliberately not chat sessions (design §10/§15).
|
|
8
|
+
*
|
|
9
|
+
* FIDELITY: pi's vanilla discovery (AGENTS.md walk to repo root, machine-global skills/extensions)
|
|
10
|
+
* is suppressed; fastagent's assembly is INJECTED into pi's session:
|
|
11
|
+
* - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
|
|
12
|
+
* (cwd) itself (including it here would duplicate it).
|
|
13
|
+
* - skills → skillsOverride (fastagent's skills, for the section + invocation).
|
|
14
|
+
* - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
|
|
15
|
+
* fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
|
|
16
|
+
* - models → a ModelRuntime with builtins only (`modelsPath: null`, no availability network), so
|
|
17
|
+
* the model surface equals serving's `createPiModels()` — pi's machine-global
|
|
18
|
+
* models.json does not leak in.
|
|
19
|
+
* - auth → fastagent's credential store at the AGENT's auth path (same resolution as the
|
|
20
|
+
* serving opener: `--auth-path`/`FASTAGENT_AUTH_PATH`, else `<root>/.secrets/auth.json`).
|
|
21
|
+
* pi's TUI `/login` writes through the injected store into the SAME file, so `fastagent
|
|
22
|
+
* login` and chat share one credential lifecycle. pi's `~/.pi` auth is not consulted.
|
|
23
|
+
*
|
|
24
|
+
* `getAgentDir()` (pi's `~/.pi`) remains ONLY for presentation-level TUI settings (theme,
|
|
25
|
+
* keybindings) — user preference, not agent definition; no auth or discovery flows from it.
|
|
26
|
+
*
|
|
27
|
+
* Cross-workspace session switches are rejected: `.env` is process-global, so one runtime is one
|
|
28
|
+
* workspace.
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { dirname, join, resolve } from "node:path";
|
|
32
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
33
|
+
import { SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import { resolveModel } from "./config.js";
|
|
35
|
+
import { assembleSystemPrompt, piBasePrompt, piDefaultTools } from "./create.js";
|
|
36
|
+
import { canonicalPath, loadAgentDefinition } from "./definition.js";
|
|
37
|
+
import { createPiModelRuntime, probeAuthSource } from "./models.js";
|
|
38
|
+
import { log } from "../../log.js";
|
|
39
|
+
import { additiveActivation, turnContext } from "./tool-context.js";
|
|
40
|
+
import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
|
|
41
|
+
import { resolveAgentAssembly } from "./open.js";
|
|
42
|
+
/** Adapt coding-agent's resident SessionManager to FastAgent's shared tool-runtime manager port. */
|
|
43
|
+
function toolChatSessionManager(session) {
|
|
44
|
+
return {
|
|
45
|
+
getSessionId: () => session.sessionManager.getSessionId(),
|
|
46
|
+
async getHeader() {
|
|
47
|
+
const header = session.sessionManager.getHeader();
|
|
48
|
+
if (!header)
|
|
49
|
+
throw new Error("chat session has no metadata header");
|
|
50
|
+
return { id: header.id, timestamp: header.timestamp };
|
|
51
|
+
},
|
|
52
|
+
async getBranch() {
|
|
53
|
+
return session.sessionManager.getBranch();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
59
|
+
* skills, and auth resolved exactly as the serving opener does). Split from the TUI launcher so the
|
|
60
|
+
* assembly — the fidelity-critical part — is inspectable and reusable without launching a TUI.
|
|
61
|
+
*/
|
|
62
|
+
export async function buildAgentSessionRuntime(dir, options = {},
|
|
63
|
+
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
64
|
+
sessionManager) {
|
|
65
|
+
/** The turn's {@link ToolActivation} over pi's AgentSession — the counterpart of invoke.ts's
|
|
66
|
+
* harness bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
|
|
67
|
+
* filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
|
|
68
|
+
* static override keeps the prompt identical to serving). */
|
|
69
|
+
function sessionToolActivation(session) {
|
|
70
|
+
// Same serialization as invoke.ts's bridge (there per turn; here per session — interactive turns
|
|
71
|
+
// make per-session equivalent): the read-modify-write below is only race-free while nothing awaits
|
|
72
|
+
// between read and write, and pi's session setters happening to be synchronous today is not a
|
|
73
|
+
// contract worth betting parallel tool batches on. Built ONCE per session (createRuntime), so
|
|
74
|
+
// parallel calls actually share the chain.
|
|
75
|
+
let chain = Promise.resolve([]);
|
|
76
|
+
return {
|
|
77
|
+
active: () => session.getActiveToolNames(),
|
|
78
|
+
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
79
|
+
activate(names) {
|
|
80
|
+
const run = async () => {
|
|
81
|
+
const current = session.getActiveToolNames();
|
|
82
|
+
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
83
|
+
if (added.length > 0)
|
|
84
|
+
session.setActiveToolsByName([...current, ...added]);
|
|
85
|
+
return added;
|
|
86
|
+
};
|
|
87
|
+
const result = chain.then(run, run); // run after the predecessor settles, success or failure
|
|
88
|
+
chain = result.catch(() => []); // the caller sees a rejection on `result`; the chain stays usable
|
|
89
|
+
return result;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolveAssembly(cwd) {
|
|
94
|
+
// The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
|
|
95
|
+
// opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
|
|
96
|
+
// (Definition→prompt assembly is NOT shared: serving re-reads live per invoke, this runtime is a
|
|
97
|
+
// startup snapshot and pi appends skills/env itself — see the header.) `tools` arrives with
|
|
98
|
+
// search_tools applied; deferral is EMULATED below like serving
|
|
99
|
+
// (what you iterate is what you serve): the initial active set excludes deferred tools (applied
|
|
100
|
+
// on the session in createRuntime — pi's session starts all-active), and the activation bridge
|
|
101
|
+
// above rides the same turn context, so the SAME search_tools works against pi's AgentSession
|
|
102
|
+
// instead of fastagent's harness.
|
|
103
|
+
const { config, modelSpec, agentDir, authPath, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
|
|
104
|
+
reportToolCollisions(toolCollisions);
|
|
105
|
+
reportModuleLoadFailures(toolFailures);
|
|
106
|
+
// ONE hub owns model resolution AND per-request auth — the ModelRuntime-shaped sibling of
|
|
107
|
+
// serving's createPiModels; see models.ts.
|
|
108
|
+
const modelRuntime = await createPiModelRuntime({ authPath });
|
|
109
|
+
// MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth;
|
|
110
|
+
// it now reads the agent's credential file like every other command. Probe the RESOLVED
|
|
111
|
+
// model's provider through the normal resolution path (stored credential OR env var — an
|
|
112
|
+
// env-authed user is fine and must not be warned): only when that provider has no usable auth
|
|
113
|
+
// AND pi's old file exists does the bare provider error get its cause named.
|
|
114
|
+
if ((await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
|
|
115
|
+
existsSync(join(getAgentDir(), "auth.json"))) {
|
|
116
|
+
log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
|
|
117
|
+
`pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
|
|
118
|
+
}
|
|
119
|
+
const model = resolveModel(modelRuntime, modelSpec);
|
|
120
|
+
const env = new NodeExecutionEnv({ cwd });
|
|
121
|
+
const definition = await loadAgentDefinition(agentDir, { cwd, env });
|
|
122
|
+
reportDefinitionWarnings(definition.collisions, definition.diagnostics);
|
|
123
|
+
const defaultNames = piDefaultTools(cwd).map((t) => t.name);
|
|
124
|
+
const customTools = tools.filter((t) => !defaultNames.includes(t.name));
|
|
125
|
+
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
|
|
126
|
+
// it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
|
|
127
|
+
// assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
|
|
128
|
+
// resolves through sessionRef at call time, exactly like the serving path resolves its harness.
|
|
129
|
+
const customToolDefs = customTools.map((t) => ({
|
|
130
|
+
name: t.name,
|
|
131
|
+
label: t.name,
|
|
132
|
+
description: t.description ?? "",
|
|
133
|
+
parameters: t.parameters,
|
|
134
|
+
// Propagate the execution mode — an activating tool (the builtin loader) declares "sequential"
|
|
135
|
+
// so pi serializes its batch; without this, pi's outer active-set diff double-stamps parallels.
|
|
136
|
+
executionMode: t.executionMode,
|
|
137
|
+
execute: (id, params, signal) => {
|
|
138
|
+
const bound = sessionRef.current;
|
|
139
|
+
// Unreachable by construction (createRuntime sets sessionRef before any turn can run a tool).
|
|
140
|
+
// Throw rather than silently run outside the turn context — that would disguise a broken
|
|
141
|
+
// session-lifecycle invariant as a normal out-of-turn call (fail visibly).
|
|
142
|
+
if (!bound)
|
|
143
|
+
throw new Error("tool executed before its session was built (lifecycle invariant broken)");
|
|
144
|
+
return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation }, () => t.execute(id, params, signal));
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
// base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
|
|
148
|
+
// them here would duplicate them).
|
|
149
|
+
const systemPrompt = assembleSystemPrompt({
|
|
150
|
+
base: piBasePrompt({ tools, persona: definition.persona }),
|
|
151
|
+
contextFiles: definition.contextFiles,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
model,
|
|
155
|
+
modelRuntime,
|
|
156
|
+
// Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
|
|
157
|
+
thinkingLevel: config.thinkingLevel,
|
|
158
|
+
definition,
|
|
159
|
+
defaultNames,
|
|
160
|
+
customTools,
|
|
161
|
+
customToolDefs,
|
|
162
|
+
deferredToolNames,
|
|
163
|
+
systemPrompt,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// pi calls the factory again on /new, /resume, switch, and fork. Config/tools dynamic imports are
|
|
167
|
+
// ESM-cached, so treating same-cwd rebuilds as hot reload would yield a half-fresh agent (fresh
|
|
168
|
+
// AGENTS.md/skills, stale config/tools). Keep the runtime a coherent startup snapshot: restart to
|
|
169
|
+
// load edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd
|
|
170
|
+
// would leak env or require mutating global env at runtime.
|
|
171
|
+
const rootCwd = canonicalPath(dir);
|
|
172
|
+
// The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
|
|
173
|
+
// while the memoized assembly (and its tool execute closures) stays. The bridge must share the
|
|
174
|
+
// session's lifetime, NOT be rebuilt per tool call (a per-call chain serializes nothing). Note on
|
|
175
|
+
// parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
|
|
176
|
+
// activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
|
|
177
|
+
// the whole batch serially and the outer diff sees correct snapshots.
|
|
178
|
+
const sessionRef = {};
|
|
179
|
+
let assembly;
|
|
180
|
+
const assemblyFor = (cwd) => {
|
|
181
|
+
// Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
|
|
182
|
+
// otherwise mismatch a non-realpath rootCwd.
|
|
183
|
+
const activeCwd = canonicalPath(cwd);
|
|
184
|
+
if (activeCwd !== rootCwd) {
|
|
185
|
+
throw workspaceScopeError(activeCwd);
|
|
186
|
+
}
|
|
187
|
+
assembly ??= resolveAssembly(rootCwd);
|
|
188
|
+
return assembly;
|
|
189
|
+
};
|
|
190
|
+
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
191
|
+
const { model, modelRuntime, thinkingLevel, definition, defaultNames, customTools, customToolDefs, deferredToolNames, systemPrompt, } = await assemblyFor(cwd);
|
|
192
|
+
const services = await createAgentSessionServices({
|
|
193
|
+
cwd,
|
|
194
|
+
// fastagent's models + auth hub replaces pi's default (~/.pi-backed) one — the auth
|
|
195
|
+
// unification point; see the header.
|
|
196
|
+
modelRuntime,
|
|
197
|
+
resourceLoaderOptions: {
|
|
198
|
+
// Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
|
|
199
|
+
// ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so this runtime runs
|
|
200
|
+
// the same agent that gets served, not the authoring machine's pi setup on top.
|
|
201
|
+
noExtensions: true,
|
|
202
|
+
noPromptTemplates: true,
|
|
203
|
+
noContextFiles: true,
|
|
204
|
+
systemPromptOverride: () => systemPrompt,
|
|
205
|
+
appendSystemPromptOverride: () => [],
|
|
206
|
+
// Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
|
|
207
|
+
// reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
|
|
208
|
+
skillsOverride: (base) => ({
|
|
209
|
+
skills: definition.skills.map((s) => ({
|
|
210
|
+
name: s.name,
|
|
211
|
+
description: s.description,
|
|
212
|
+
filePath: s.filePath,
|
|
213
|
+
baseDir: dirname(s.filePath),
|
|
214
|
+
sourceInfo: {
|
|
215
|
+
path: s.filePath,
|
|
216
|
+
source: "fastagent",
|
|
217
|
+
scope: "project",
|
|
218
|
+
origin: "top-level",
|
|
219
|
+
baseDir: dirname(s.filePath),
|
|
220
|
+
},
|
|
221
|
+
disableModelInvocation: s.disableModelInvocation ?? false,
|
|
222
|
+
})),
|
|
223
|
+
diagnostics: base.diagnostics,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const result = await createAgentSessionFromServices({
|
|
228
|
+
services,
|
|
229
|
+
sessionManager,
|
|
230
|
+
sessionStartEvent,
|
|
231
|
+
model,
|
|
232
|
+
thinkingLevel,
|
|
233
|
+
tools: [...defaultNames, ...customTools.map((t) => t.name)],
|
|
234
|
+
customTools: customToolDefs,
|
|
235
|
+
});
|
|
236
|
+
sessionRef.current = {
|
|
237
|
+
session: result.session,
|
|
238
|
+
sessionManager: toolChatSessionManager(result.session),
|
|
239
|
+
activation: sessionToolActivation(result.session),
|
|
240
|
+
};
|
|
241
|
+
// Deferral emulation: pi's session starts with everything active — narrow it by SUBTRACTING
|
|
242
|
+
// the deferred names from whatever is active (robust to pi mounting tools of its own; an
|
|
243
|
+
// exact-set-equality gate would silently stop narrowing the day pi adds one). Applied on EVERY
|
|
244
|
+
// build including /resume: pi's chat session does not record activations (its SessionContext has
|
|
245
|
+
// no activeToolNames), so "restore prior activations" is not implementable here — deferral stays
|
|
246
|
+
// consistently ON and a resumed conversation re-discovers via search_tools (documented divergence
|
|
247
|
+
// from serving, where activations persist in the session). The deferred SET comes from the shared
|
|
248
|
+
// assembly (one definition of "deferred"), never recomputed here.
|
|
249
|
+
if (deferredToolNames.length > 0) {
|
|
250
|
+
const active = result.session.getActiveToolNames();
|
|
251
|
+
if (deferredToolNames.some((n) => active.includes(n))) {
|
|
252
|
+
result.session.setActiveToolsByName(active.filter((n) => !deferredToolNames.includes(n)));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
256
|
+
};
|
|
257
|
+
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
258
|
+
cwd: rootCwd,
|
|
259
|
+
agentDir: getAgentDir(),
|
|
260
|
+
sessionManager: sessionManager ?? SessionManager.create(rootCwd),
|
|
261
|
+
});
|
|
262
|
+
enforceWorkspaceScopedSessionSwitches(runtime, rootCwd);
|
|
263
|
+
return runtime;
|
|
264
|
+
}
|
|
265
|
+
function workspaceScopeError(targetCwd) {
|
|
266
|
+
return new Error(`fastagent sessions are workspace-scoped: cannot switch to ${targetCwd}; open that workspace instead`);
|
|
267
|
+
}
|
|
268
|
+
function readSessionHeaderCwd(sessionPath) {
|
|
269
|
+
const resolvedPath = resolve(sessionPath);
|
|
270
|
+
if (!existsSync(resolvedPath))
|
|
271
|
+
return undefined;
|
|
272
|
+
for (const line of readFileSync(resolvedPath, "utf8").split(/\r?\n/)) {
|
|
273
|
+
if (!line.trim())
|
|
274
|
+
continue;
|
|
275
|
+
try {
|
|
276
|
+
const entry = JSON.parse(line);
|
|
277
|
+
if (entry.type === "session")
|
|
278
|
+
return typeof entry.cwd === "string" ? canonicalPath(entry.cwd) : undefined;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Ignore malformed lines the same way pi's session loader does; no header cwd → caller pins root.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Keep resume/import inside the runtime's single workspace, deciding BEFORE delegating to pi. The
|
|
288
|
+
* process is chdir'd into rootCwd, so a session with no cwd header already lands on rootCwd. The gap
|
|
289
|
+
* is a session that EXPLICITLY records a different cwd: pi would bind it and the factory would reject
|
|
290
|
+
* it — but only AFTER tearing the live session down. Reject such a switch up front.
|
|
291
|
+
*/
|
|
292
|
+
function enforceWorkspaceScopedSessionSwitches(runtime, rootCwd) {
|
|
293
|
+
const rejectForeignTarget = (sessionPath, cwdOverride) => {
|
|
294
|
+
const target = cwdOverride !== undefined ? canonicalPath(cwdOverride) : readSessionHeaderCwd(sessionPath);
|
|
295
|
+
if (target !== undefined && target !== rootCwd)
|
|
296
|
+
throw workspaceScopeError(target);
|
|
297
|
+
};
|
|
298
|
+
const switchSession = runtime.switchSession.bind(runtime);
|
|
299
|
+
runtime.switchSession = async (...args) => {
|
|
300
|
+
rejectForeignTarget(args[0], args[1]?.cwdOverride);
|
|
301
|
+
return switchSession(...args);
|
|
302
|
+
};
|
|
303
|
+
const importFromJsonl = runtime.importFromJsonl.bind(runtime);
|
|
304
|
+
runtime.importFromJsonl = async (...args) => {
|
|
305
|
+
rejectForeignTarget(args[0], args[1]);
|
|
306
|
+
return importFromJsonl(...args);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Models } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type SessionControl, type SessionEvent } from "../../session.ts";
|
|
3
|
+
import type { Lease, SessionObserver } from "./invoke.ts";
|
|
4
|
+
import { type PiHarnessFactory } from "./harness.ts";
|
|
5
|
+
import type { PiSessionReader } from "./sessions.ts";
|
|
6
|
+
/** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
|
|
7
|
+
* connection — the wire's ReadableStream backpressure stops pulling while invokes keep pushing)
|
|
8
|
+
* has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
|
|
9
|
+
* MB worst case per stuck connection) and its subscription marked closed. The close is observed
|
|
10
|
+
* via pulls — which a stalled connection by definition does not make — so a consumer that RESUMES
|
|
11
|
+
* pulling first drains the frozen backlog, then gets done (no buffered event dropped), while a
|
|
12
|
+
* permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
|
|
13
|
+
* way is the standard reconnect+backfill, semantically lossless. */
|
|
14
|
+
export declare const SUBSCRIBER_BUFFER_CAP = 10000;
|
|
15
|
+
/** What boundary mutations (compact / set_model / set_thinking) need — the SAME instances the
|
|
16
|
+
* agent assembly uses: the lease (mutations must not race a run), the model registry (validation +
|
|
17
|
+
* allowedModels), and the harness factory (compaction is a model call). Writes go through the
|
|
18
|
+
* session the hub's reader opened — after an existence check, so the control plane never creates
|
|
19
|
+
* a session (that is the data plane's monopoly). */
|
|
20
|
+
export interface PiBoundaryWiring {
|
|
21
|
+
lease: Lease;
|
|
22
|
+
models: Models;
|
|
23
|
+
harnessFactory: PiHarnessFactory;
|
|
24
|
+
}
|
|
25
|
+
export interface CreatePiSessionControlOptions {
|
|
26
|
+
/** Read-only access to the durable session repository (the same root the agent writes). */
|
|
27
|
+
sessions: PiSessionReader;
|
|
28
|
+
/** Boundary-mutation wiring, as a LAZY thunk: the hub's observer must exist before the agent
|
|
29
|
+
* assembly that produces these parts, so the hub asks for them at dispatch time instead
|
|
30
|
+
* (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands
|
|
31
|
+
* are gated off in `capabilities()` and rejected `unsupported_capability`. */
|
|
32
|
+
boundary?: () => PiBoundaryWiring | undefined;
|
|
33
|
+
/** Tap for the events the HUB ITSELF generates (boundary mutations: `state_changed`,
|
|
34
|
+
* `compaction_*`) — those never pass through the data plane's observer seam, so a consumer
|
|
35
|
+
* composing a full-vocabulary tap wires the run events via the observer AND this. Called after
|
|
36
|
+
* the hub's own subscribers. */
|
|
37
|
+
tap?: (session: string, event: SessionEvent) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the observation hub. Wire `observer` into the SAME agent assembly that serves the sessions:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* const { control, observer } = createPiSessionControl({ sessions });
|
|
44
|
+
* const agent = createPiAgent({ ..., sessions, observer });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createPiSessionControl(options: CreatePiSessionControlOptions): {
|
|
48
|
+
control: SessionControl;
|
|
49
|
+
observer: SessionObserver;
|
|
50
|
+
};
|