@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
package/dist/engines/pi/auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth for the pi engine: a read-WRITE {@link CredentialStore} over a fastagent credentials file,
|
|
3
3
|
* consumed by the `Models` collection (models.ts). The path is project-level by default
|
|
4
|
-
* (`<
|
|
5
|
-
* location used as the override target / login default, not an implicit per-provider fallback.
|
|
4
|
+
* (`<agentDir>/.secrets/auth.json`, resolved by the opener); {@link GLOBAL_AUTH_PATH} is the
|
|
5
|
+
* global location used as the override target / login default, not an implicit per-provider fallback.
|
|
6
6
|
*
|
|
7
7
|
* Project-level default + NO implicit project↔global fallback, for two reasons: (1) isolation — each
|
|
8
8
|
* agent can use a different account/subscription; (2) fail-visibly — a missing credential surfaces at
|
|
@@ -27,18 +27,20 @@
|
|
|
27
27
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
|
+
import { GLOBAL_HOME_DIR, SECRETS_DIRNAME } from "../../paths.js";
|
|
30
31
|
import { log } from "../../log.js";
|
|
31
32
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
32
33
|
import lockfile from "proper-lockfile";
|
|
33
34
|
/**
|
|
34
|
-
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
|
|
36
|
+
* home `~/.fastagent/` — which carries the same unified shape as a workspace (`.secrets/auth.json`).
|
|
37
|
+
* The project-level default is `<agentDir>/.secrets/auth.json` (computed by the opener and by
|
|
38
|
+
* `fastagent login`); this is only the `loginFlow()` PROGRAMMATIC fallback (when a caller omits
|
|
39
|
+
* `authPath`) and the path to point `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE
|
|
40
|
+
* credential file across projects (safe — one file, one lock-serialized refresh lifecycle). The
|
|
41
|
+
* `fastagent login` CLI is project-level by default, never this.
|
|
40
42
|
*/
|
|
41
|
-
export const GLOBAL_AUTH_PATH = join(homedir(),
|
|
43
|
+
export const GLOBAL_AUTH_PATH = join(homedir(), GLOBAL_HOME_DIR, SECRETS_DIRNAME, "auth.json");
|
|
42
44
|
/** A valid stored credential, or undefined — a foreign/old entry reads as not-configured, not a crash. */
|
|
43
45
|
function pick(creds, providerId) {
|
|
44
46
|
const cred = creds[providerId];
|
|
@@ -183,7 +185,7 @@ function parseForWrite(raw, where) {
|
|
|
183
185
|
return creds;
|
|
184
186
|
}
|
|
185
187
|
/** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
|
|
186
|
-
* the directory opener passes the project-level `<
|
|
188
|
+
* the directory opener passes the project-level `<root>/.secrets/auth.json`). */
|
|
187
189
|
export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options = {}) {
|
|
188
190
|
const warn = options.warn ?? ((message) => log.warn(message));
|
|
189
191
|
return {
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
import { type ChannelContext, type Routes } from "../../host/node.ts";
|
|
1
|
+
import { type ChannelContext, type LongConnection, type Routes } from "../../host/node.ts";
|
|
2
2
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
3
3
|
/** A dropped route: two channels claim the same key. Surfaced, never silent. */
|
|
4
4
|
export interface ChannelCollision {
|
|
5
5
|
route: string;
|
|
6
6
|
source: string;
|
|
7
7
|
}
|
|
8
|
+
/** A long-connection module bound to the same context route factories receive. Internal serving shape. */
|
|
9
|
+
export interface LoadedLongConnectionChannel {
|
|
10
|
+
name: string;
|
|
11
|
+
connect(signal: AbortSignal): LongConnection;
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* here too. This path is independent of loadChannels', so it must guard the boundary on its own.
|
|
14
|
+
* Import channel files without mounting route factories or opening connections. Deployment needs only
|
|
15
|
+
* the authored structural fact: function exports are route channels; `{ connect() }` exports are
|
|
16
|
+
* long-connection channels. There is no second ingress/lifecycle declaration to keep in sync.
|
|
13
17
|
*/
|
|
14
|
-
export declare function
|
|
18
|
+
export declare function inspectChannels(dir: string): Promise<{
|
|
19
|
+
channels: string[];
|
|
20
|
+
routeChannels: string[];
|
|
21
|
+
longConnectionChannels: string[];
|
|
22
|
+
failures: ModuleLoadFailure[];
|
|
23
|
+
}>;
|
|
15
24
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* merged (first file wins a route-key clash, the dropped route surfaced).
|
|
19
|
-
*
|
|
20
|
-
* A channel file broken for ANY reason — a failed import, a factory that throws when called (a missing
|
|
21
|
-
* env var is the common deploy case), or a malformed shape (not a function, not a Routes object, a bad
|
|
22
|
-
* handler/route key) — is collected in `failures` without preventing validation of sibling files. The
|
|
23
|
-
* serving CLI treats any such failure as fatal: a declared channel must not silently disappear or cause
|
|
24
|
-
* the default `/invoke` route to mount. Programmatic callers can inspect the returned data themselves.
|
|
25
|
-
* Routes are validated fully before any merge, so a throw mounts no partial routes.
|
|
25
|
+
* Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
|
|
26
|
+
* WITHOUT importing. A symlinked channels directory must remain inside the agent dir.
|
|
26
27
|
*/
|
|
28
|
+
export declare function discoverChannelFiles(dir: string): Promise<string[]>;
|
|
29
|
+
/** Discover, validate, and bind all channel modules. No long connection is opened here; the CLI owns it. */
|
|
27
30
|
export declare function loadChannels(dir: string, ctx: ChannelContext): Promise<{
|
|
28
31
|
routes: Routes;
|
|
32
|
+
longConnections: LoadedLongConnectionChannel[];
|
|
33
|
+
routeChannels: string[];
|
|
34
|
+
longConnectionChannels: string[];
|
|
29
35
|
collisions: ChannelCollision[];
|
|
30
36
|
failures: ModuleLoadFailure[];
|
|
31
37
|
}>;
|
|
@@ -1,22 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Channel discovery (the N axis, filesystem form)
|
|
3
|
-
*
|
|
4
|
-
* app's `on()` glue and returns the routes it mounts. There is no config-level channel list — a
|
|
5
|
-
* channel always needs glue, so it is always a file.
|
|
2
|
+
* Channel discovery (the N axis, filesystem form). A channel file default-exports either the existing
|
|
3
|
+
* route factory `(ctx) => Routes`, or an explicit long-connection module `{ name, connect(ctx, signal) }`.
|
|
6
4
|
*/
|
|
7
5
|
import { readdir } from "node:fs/promises";
|
|
8
6
|
import { isAbsolute, join } from "node:path";
|
|
9
|
-
import { parseRouteKey } from "../../host/node.js";
|
|
10
|
-
import { assertInsideWorkspace } from "../../workspace.js";
|
|
7
|
+
import { parseRouteKey, } from "../../host/node.js";
|
|
11
8
|
import { isModuleFile, loadModuleDir } from "../../loader.js";
|
|
9
|
+
import { assertInsideAgentDir } from "../../paths.js";
|
|
10
|
+
function longConnectionModule(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && typeof value.connect === "function";
|
|
12
|
+
}
|
|
13
|
+
function validateLongConnectionModule(value, label) {
|
|
14
|
+
if (typeof value.name !== "string" || value.name.trim() === "") {
|
|
15
|
+
throw new Error(`${label}: long-connection channel name must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Import channel files without mounting route factories or opening connections. Deployment needs only
|
|
20
|
+
* the authored structural fact: function exports are route channels; `{ connect() }` exports are
|
|
21
|
+
* long-connection channels. There is no second ingress/lifecycle declaration to keep in sync.
|
|
22
|
+
*/
|
|
23
|
+
export async function inspectChannels(dir) {
|
|
24
|
+
await assertInsideAgentDir(dir, "channels");
|
|
25
|
+
const { modules, failures } = await loadModuleDir(join(dir, "channels"));
|
|
26
|
+
const channels = [];
|
|
27
|
+
const routeChannels = [];
|
|
28
|
+
const longConnectionChannels = [];
|
|
29
|
+
for (const { name, label, file, mod } of modules) {
|
|
30
|
+
try {
|
|
31
|
+
if (typeof mod.default === "function") {
|
|
32
|
+
channels.push(name);
|
|
33
|
+
routeChannels.push(name);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (longConnectionModule(mod.default)) {
|
|
37
|
+
validateLongConnectionModule(mod.default, label);
|
|
38
|
+
channels.push(name);
|
|
39
|
+
longConnectionChannels.push(name);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`${label} must default-export (ctx) => Routes or { name, connect(ctx, signal) }`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
failures.push({ label, file, message: error.message });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { channels, routeChannels, longConnectionChannels, failures };
|
|
49
|
+
}
|
|
12
50
|
/**
|
|
13
51
|
* Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
|
|
14
|
-
* WITHOUT importing
|
|
15
|
-
* exactly the surface dev/start would accept: a channels/ symlink escaping the workspace is rejected
|
|
16
|
-
* here too. This path is independent of loadChannels', so it must guard the boundary on its own.
|
|
52
|
+
* WITHOUT importing. A symlinked channels directory must remain inside the agent dir.
|
|
17
53
|
*/
|
|
18
54
|
export async function discoverChannelFiles(dir) {
|
|
19
|
-
await
|
|
55
|
+
await assertInsideAgentDir(dir, "channels");
|
|
20
56
|
let names;
|
|
21
57
|
try {
|
|
22
58
|
names = await readdir(join(dir, "channels"));
|
|
@@ -28,75 +64,68 @@ export async function discoverChannelFiles(dir) {
|
|
|
28
64
|
}
|
|
29
65
|
return names
|
|
30
66
|
.filter(isModuleFile)
|
|
31
|
-
.map((
|
|
67
|
+
.map((name) => name.replace(/\.(ts|js|mjs)$/, ""))
|
|
32
68
|
.sort();
|
|
33
69
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
function validateRoutes(value, label) {
|
|
71
|
+
if (value === null || typeof value !== "object" || value instanceof Map) {
|
|
72
|
+
throw new Error(`${label} must return a Routes object`);
|
|
73
|
+
}
|
|
74
|
+
const routes = Object.entries(value);
|
|
75
|
+
if (routes.length === 0) {
|
|
76
|
+
throw new Error(`${label} declared no routes — return a non-empty { "METHOD /path": handler } object`);
|
|
77
|
+
}
|
|
78
|
+
for (const [route, handler] of routes) {
|
|
79
|
+
if (typeof handler !== "function") {
|
|
80
|
+
throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
|
|
81
|
+
}
|
|
82
|
+
if (!parseRouteKey(route).path.startsWith("/")) {
|
|
83
|
+
throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return routes;
|
|
87
|
+
}
|
|
88
|
+
/** Discover, validate, and bind all channel modules. No long connection is opened here; the CLI owns it. */
|
|
46
89
|
export async function loadChannels(dir, ctx) {
|
|
47
|
-
// The contract says stateRoot is absolute; enforce it at the mount boundary so a relative root fails
|
|
48
|
-
// fast HERE instead of silently re-anchoring some channel's state on the process cwd.
|
|
49
90
|
if (!isAbsolute(ctx.stateRoot)) {
|
|
50
91
|
throw new Error(`ChannelContext.stateRoot must be absolute, got "${ctx.stateRoot}"`);
|
|
51
92
|
}
|
|
52
|
-
|
|
53
|
-
// the dir includes it (the directory is the agent).
|
|
54
|
-
await assertInsideWorkspace(dir, "channels");
|
|
93
|
+
await assertInsideAgentDir(dir, "channels");
|
|
55
94
|
const { modules, failures } = await loadModuleDir(join(dir, "channels"));
|
|
56
95
|
const routes = {};
|
|
96
|
+
const longConnections = [];
|
|
97
|
+
const routeChannels = [];
|
|
98
|
+
const longConnectionChannels = [];
|
|
57
99
|
const collisions = [];
|
|
58
|
-
for (const { label, file, mod } of modules) {
|
|
59
|
-
// Collect every per-file failure so the caller can report all broken channels in one pass. The CLI
|
|
60
|
-
// then fails startup rather than silently dropping a declared route; direct callers own their policy.
|
|
61
|
-
// Routes are VALIDATED fully before any are merged, so a throw mid-validation mounts NO partial routes.
|
|
100
|
+
for (const { name, label, file, mod } of modules) {
|
|
62
101
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
if (longConnectionModule(mod.default)) {
|
|
103
|
+
validateLongConnectionModule(mod.default, label);
|
|
104
|
+
const channel = mod.default;
|
|
105
|
+
longConnections.push({
|
|
106
|
+
name: channel.name,
|
|
107
|
+
connect: (signal) => channel.connect(ctx, signal),
|
|
108
|
+
});
|
|
109
|
+
longConnectionChannels.push(name);
|
|
110
|
+
continue;
|
|
66
111
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
if (typeof mod.default !== "function") {
|
|
113
|
+
throw new Error(`${label} must default-export (ctx) => Routes or { name, connect(ctx, signal) }`);
|
|
114
|
+
}
|
|
115
|
+
const declared = mod.default(ctx);
|
|
70
116
|
if (declared !== null &&
|
|
71
117
|
typeof declared === "object" &&
|
|
72
118
|
typeof declared.then === "function") {
|
|
73
119
|
declared.catch(() => { });
|
|
74
|
-
throw new Error(`${label} must return Routes synchronously, not a Promise
|
|
75
|
-
}
|
|
76
|
-
if (declared === null || typeof declared !== "object") {
|
|
77
|
-
throw new Error(`${label} must return a Routes object, got ${declared === null ? "null" : typeof declared}`);
|
|
78
|
-
}
|
|
79
|
-
const declaredRoutes = Object.entries(declared);
|
|
80
|
-
if (declaredRoutes.length === 0) {
|
|
81
|
-
throw new Error(`${label} declared no routes — return a non-empty { "METHOD /path": handler } object (a Promise, Map, array, or {} yields none)`);
|
|
82
|
-
}
|
|
83
|
-
// Validate every route BEFORE merging any (no partial mount on a later throw).
|
|
84
|
-
for (const [route, handler] of declaredRoutes) {
|
|
85
|
-
if (typeof handler !== "function") {
|
|
86
|
-
throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
|
|
87
|
-
}
|
|
88
|
-
if (!parseRouteKey(route).path.startsWith("/")) {
|
|
89
|
-
throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
|
|
90
|
-
}
|
|
120
|
+
throw new Error(`${label} must return Routes synchronously, not a Promise`);
|
|
91
121
|
}
|
|
122
|
+
const declaredRoutes = validateRoutes(declared, label);
|
|
92
123
|
for (const [route, handler] of declaredRoutes) {
|
|
93
124
|
const parsed = parseRouteKey(route);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return (e.path === parsed.path &&
|
|
99
|
-
(e.method === undefined || parsed.method === undefined || e.method === parsed.method));
|
|
125
|
+
const clash = Object.keys(routes).some((key) => {
|
|
126
|
+
const existing = parseRouteKey(key);
|
|
127
|
+
return (existing.path === parsed.path &&
|
|
128
|
+
(existing.method === undefined || parsed.method === undefined || existing.method === parsed.method));
|
|
100
129
|
});
|
|
101
130
|
if (clash) {
|
|
102
131
|
collisions.push({ route, source: label });
|
|
@@ -104,10 +133,11 @@ export async function loadChannels(dir, ctx) {
|
|
|
104
133
|
}
|
|
105
134
|
routes[route] = handler;
|
|
106
135
|
}
|
|
136
|
+
routeChannels.push(name);
|
|
107
137
|
}
|
|
108
138
|
catch (error) {
|
|
109
139
|
failures.push({ label, file, message: error.message });
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
|
-
return { routes, collisions, failures };
|
|
142
|
+
return { routes, longConnections, routeChannels, longConnectionChannels, collisions, failures };
|
|
113
143
|
}
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
export interface RunPiChatOptions {
|
|
3
|
-
/** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
4
|
-
model?: string;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
8
|
-
* skills resolved exactly as the dev opener does). Split from {@link runPiChat} so the assembly —
|
|
9
|
-
* the fidelity-critical part — is inspectable without launching the TUI.
|
|
10
|
-
*/
|
|
11
|
-
export declare function buildChatRuntime(dir: string, options?: RunPiChatOptions,
|
|
12
|
-
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
13
|
-
sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
|
|
1
|
+
import { type BuildSessionRuntimeOptions } from "./session-builder.ts";
|
|
14
2
|
/**
|
|
15
3
|
* Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
|
|
16
|
-
* fastagent's assembled agent (same model/tools/skills/prompt as dev/start serve); pi's TUI
|
|
17
|
-
* login, rendering, and same-workspace sessions natively.
|
|
4
|
+
* fastagent's assembled agent (same model/tools/skills/prompt/auth as dev/start serve); pi's TUI
|
|
5
|
+
* handles login, rendering, and same-workspace sessions natively.
|
|
18
6
|
*/
|
|
19
|
-
export declare function runPiChat(dir: string, options?:
|
|
7
|
+
export declare function runPiChat(dir: string, options?: BuildSessionRuntimeOptions): Promise<void>;
|
package/dist/engines/pi/chat.js
CHANGED
|
@@ -3,271 +3,18 @@
|
|
|
3
3
|
* engine-neutral channel: it drives pi's full session API (InteractiveMode) for fidelity, so it lives
|
|
4
4
|
* under engines/pi/ and is not re-exported.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
|
|
10
|
-
* (cwd) itself (including it here would duplicate it).
|
|
11
|
-
* - skills → skillsOverride (fastagent's skills, for the section + invocation).
|
|
12
|
-
* - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
|
|
13
|
-
* fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
|
|
14
|
-
*
|
|
15
|
-
* Cross-workspace session switches are rejected: `.env` is process-global, so one chat TUI is one
|
|
16
|
-
* workspace.
|
|
17
|
-
*
|
|
18
|
-
* AUTH: chat is the one command that does NOT use fastagent's credential file. It drives pi's own
|
|
19
|
-
* session services (`createAgentSessionServices`, auth from pi's `~/.pi` via `getAgentDir()`), so you
|
|
20
|
-
* log in through pi's TUI, not `fastagent login`. `--auth-path`/`FASTAGENT_AUTH_PATH` therefore do not
|
|
21
|
-
* apply here, and `createPiModels()` below is used only to RESOLVE the model descriptor (never for auth).
|
|
22
|
-
*/
|
|
23
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
-
import { dirname, resolve } from "node:path";
|
|
25
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
26
|
-
import { InteractiveMode, SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
27
|
-
import { loadConfig, resolveAgentDir, resolveModel, resolveModelSpec } from "./config.js";
|
|
28
|
-
import { assembleSystemPrompt, piBasePrompt, piDefaultTools, resolveTools } from "./create.js";
|
|
29
|
-
import { createPiModels } from "./models.js";
|
|
30
|
-
import { canonicalPath, loadAgentDefinition } from "./definition.js";
|
|
31
|
-
import { isDeferredTool, loadTools, mergeDiscoveredTools } from "./tool.js";
|
|
32
|
-
import { withSearchTool } from "./search-tools.js";
|
|
33
|
-
import { additiveActivation, turnContext } from "./tool-context.js";
|
|
34
|
-
import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
|
|
35
|
-
/**
|
|
36
|
-
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
37
|
-
* skills resolved exactly as the dev opener does). Split from {@link runPiChat} so the assembly —
|
|
38
|
-
* the fidelity-critical part — is inspectable without launching the TUI.
|
|
39
|
-
*/
|
|
40
|
-
export async function buildChatRuntime(dir, options = {},
|
|
41
|
-
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
42
|
-
sessionManager) {
|
|
43
|
-
/** The turn's {@link ToolActivation} over pi's AgentSession — the chat counterpart of invoke.ts's
|
|
44
|
-
* harness bridge, so the SAME builtin search_tools serves both paths. Additive; unknown names
|
|
45
|
-
* filtered (`setActiveToolsByName` is authoritative on the session and rebuilds its prompt — our
|
|
46
|
-
* static override keeps the prompt identical to serving). */
|
|
47
|
-
function chatToolActivation(session) {
|
|
48
|
-
// Same serialization as invoke.ts's bridge (there per turn; here per session — chat turns are
|
|
49
|
-
// interactive, so per-session is equivalent): the read-modify-write below is only race-free while
|
|
50
|
-
// nothing awaits between read and write, and pi's session setters happening to be synchronous today
|
|
51
|
-
// is not a contract worth betting parallel tool batches on. Built ONCE per session (createRuntime),
|
|
52
|
-
// so parallel calls actually share the chain.
|
|
53
|
-
let chain = Promise.resolve([]);
|
|
54
|
-
return {
|
|
55
|
-
active: () => session.getActiveToolNames(),
|
|
56
|
-
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
57
|
-
activate(names) {
|
|
58
|
-
const run = async () => {
|
|
59
|
-
const current = session.getActiveToolNames();
|
|
60
|
-
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
61
|
-
if (added.length > 0)
|
|
62
|
-
session.setActiveToolsByName([...current, ...added]);
|
|
63
|
-
return added;
|
|
64
|
-
};
|
|
65
|
-
const result = chain.then(run, run); // run after the predecessor settles, success or failure
|
|
66
|
-
chain = result.catch(() => []); // the caller sees a rejection on `result`; the chain stays usable
|
|
67
|
-
return result;
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
async function resolveAssembly(cwd) {
|
|
72
|
-
const { config } = await loadConfig(cwd);
|
|
73
|
-
const modelSpec = resolveModelSpec(options.model, config);
|
|
74
|
-
if (!modelSpec) {
|
|
75
|
-
throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
76
|
-
}
|
|
77
|
-
// Resolution only — the Models' auth is unused here; chat's auth is pi's own (~/.pi via the session
|
|
78
|
-
// services), so authPath is intentionally not threaded in. See the AUTH note in the header.
|
|
79
|
-
const model = resolveModel(createPiModels(), modelSpec);
|
|
80
|
-
const env = new NodeExecutionEnv({ cwd });
|
|
81
|
-
// Same agentDir/cwd split as dev/start: persona/skills/tools from agentDir, ② context walked from cwd.
|
|
82
|
-
const agentDir = resolveAgentDir(cwd, config);
|
|
83
|
-
const definition = await loadAgentDefinition(agentDir, { cwd, env });
|
|
84
|
-
reportDefinitionWarnings(definition.collisions, definition.diagnostics);
|
|
85
|
-
// Same tool resolution as the dev opener, then split: defaults go to pi by NAME (rebuilt cwd-bound
|
|
86
|
-
// for rich rendering); customs go through pi's `customTools` path so they survive /new, /resume, fork.
|
|
87
|
-
const discovered = await loadTools(agentDir);
|
|
88
|
-
const merged = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
|
|
89
|
-
// Chat EMULATES deferral, like serving (what you iterate is what you serve): the builtin loader
|
|
90
|
-
// mounts when a deferred tool exists, the initial active set excludes deferred tools (applied on
|
|
91
|
-
// the session in createRuntime below — pi's TUI session starts all-active), and the activation
|
|
92
|
-
// bridge below rides the same turn context the serving path uses, so the SAME search_tools works
|
|
93
|
-
// against pi's AgentSession instead of fastagent's harness.
|
|
94
|
-
const tools = withSearchTool(merged.tools);
|
|
95
|
-
const crossCollisions = merged.collisions;
|
|
96
|
-
reportToolCollisions([...discovered.collisions, ...crossCollisions]);
|
|
97
|
-
reportModuleLoadFailures(discovered.failures);
|
|
98
|
-
const defaultNames = piDefaultTools(cwd).map((t) => t.name);
|
|
99
|
-
const customTools = tools.filter((t) => !defaultNames.includes(t.name));
|
|
100
|
-
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
|
|
101
|
-
// it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
|
|
102
|
-
// assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
|
|
103
|
-
// resolves through sessionRef at call time, exactly like the serving path resolves its harness.
|
|
104
|
-
const customToolDefs = customTools.map((t) => ({
|
|
105
|
-
name: t.name,
|
|
106
|
-
label: t.name,
|
|
107
|
-
description: t.description ?? "",
|
|
108
|
-
parameters: t.parameters,
|
|
109
|
-
// Propagate the execution mode — an activating tool (the builtin loader) declares "sequential"
|
|
110
|
-
// so pi serializes its batch; without this, pi's outer active-set diff double-stamps parallels.
|
|
111
|
-
executionMode: t.executionMode,
|
|
112
|
-
execute: (id, params, signal) => {
|
|
113
|
-
const bound = sessionRef.current;
|
|
114
|
-
// Unreachable by construction (createRuntime sets sessionRef before any turn can run a tool).
|
|
115
|
-
// Throw rather than silently run outside the turn context — that would disguise a broken
|
|
116
|
-
// session-lifecycle invariant as a normal out-of-turn call (fail visibly).
|
|
117
|
-
if (!bound)
|
|
118
|
-
throw new Error("chat tool executed before its session was built (lifecycle invariant broken)");
|
|
119
|
-
return turnContext.run({ session: bound.session.sessionId, tools: bound.activation }, () => t.execute(id, params, signal));
|
|
120
|
-
},
|
|
121
|
-
}));
|
|
122
|
-
// base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
|
|
123
|
-
// them here would duplicate them).
|
|
124
|
-
const systemPrompt = assembleSystemPrompt({
|
|
125
|
-
base: piBasePrompt({ tools, persona: definition.persona }),
|
|
126
|
-
contextFiles: definition.contextFiles,
|
|
127
|
-
});
|
|
128
|
-
return { model, definition, defaultNames, customTools, customToolDefs, systemPrompt };
|
|
129
|
-
}
|
|
130
|
-
// pi calls the factory again on /new, /resume, switch, and fork. Config/tools dynamic imports are
|
|
131
|
-
// ESM-cached, so treating same-cwd rebuilds as hot reload would yield a half-fresh agent (fresh
|
|
132
|
-
// AGENTS.md/skills, stale config/tools). Keep chat a coherent startup snapshot: restart to load
|
|
133
|
-
// edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd would
|
|
134
|
-
// leak env or require mutating global env at runtime.
|
|
135
|
-
const rootCwd = canonicalPath(dir);
|
|
136
|
-
// The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
|
|
137
|
-
// while the memoized assembly (and its tool execute closures) stays. The bridge must share the
|
|
138
|
-
// session's lifetime, NOT be rebuilt per tool call (a per-call chain serializes nothing). Note on
|
|
139
|
-
// parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
|
|
140
|
-
// activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
|
|
141
|
-
// the whole batch serially and the outer diff sees correct snapshots.
|
|
142
|
-
const sessionRef = {};
|
|
143
|
-
let assembly;
|
|
144
|
-
const assemblyFor = (cwd) => {
|
|
145
|
-
// Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
|
|
146
|
-
// otherwise mismatch a non-realpath rootCwd.
|
|
147
|
-
const activeCwd = canonicalPath(cwd);
|
|
148
|
-
if (activeCwd !== rootCwd) {
|
|
149
|
-
throw workspaceScopeError(activeCwd);
|
|
150
|
-
}
|
|
151
|
-
assembly ??= resolveAssembly(rootCwd);
|
|
152
|
-
return assembly;
|
|
153
|
-
};
|
|
154
|
-
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
155
|
-
const { model, definition, defaultNames, customTools, customToolDefs, systemPrompt } = await assemblyFor(cwd);
|
|
156
|
-
const services = await createAgentSessionServices({
|
|
157
|
-
cwd,
|
|
158
|
-
resourceLoaderOptions: {
|
|
159
|
-
// Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
|
|
160
|
-
// ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so chat runs the same
|
|
161
|
-
// agent that gets served, not the authoring machine's pi setup on top.
|
|
162
|
-
noExtensions: true,
|
|
163
|
-
noPromptTemplates: true,
|
|
164
|
-
noContextFiles: true,
|
|
165
|
-
systemPromptOverride: () => systemPrompt,
|
|
166
|
-
appendSystemPromptOverride: () => [],
|
|
167
|
-
// Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
|
|
168
|
-
// reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
|
|
169
|
-
skillsOverride: (base) => ({
|
|
170
|
-
skills: definition.skills.map((s) => ({
|
|
171
|
-
name: s.name,
|
|
172
|
-
description: s.description,
|
|
173
|
-
filePath: s.filePath,
|
|
174
|
-
baseDir: dirname(s.filePath),
|
|
175
|
-
sourceInfo: {
|
|
176
|
-
path: s.filePath,
|
|
177
|
-
source: "fastagent",
|
|
178
|
-
scope: "project",
|
|
179
|
-
origin: "top-level",
|
|
180
|
-
baseDir: dirname(s.filePath),
|
|
181
|
-
},
|
|
182
|
-
disableModelInvocation: s.disableModelInvocation ?? false,
|
|
183
|
-
})),
|
|
184
|
-
diagnostics: base.diagnostics,
|
|
185
|
-
}),
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
const result = await createAgentSessionFromServices({
|
|
189
|
-
services,
|
|
190
|
-
sessionManager,
|
|
191
|
-
sessionStartEvent,
|
|
192
|
-
model,
|
|
193
|
-
tools: [...defaultNames, ...customTools.map((t) => t.name)],
|
|
194
|
-
customTools: customToolDefs,
|
|
195
|
-
});
|
|
196
|
-
sessionRef.current = { session: result.session, activation: chatToolActivation(result.session) };
|
|
197
|
-
// Deferral emulation: pi's TUI session starts with everything active — narrow it by SUBTRACTING
|
|
198
|
-
// the deferred names from whatever is active (robust to pi mounting tools of its own; an
|
|
199
|
-
// exact-set-equality gate would silently stop narrowing the day pi adds one). Applied on EVERY
|
|
200
|
-
// build including /resume: pi's chat session does not record activations (its SessionContext has
|
|
201
|
-
// no activeToolNames), so "restore prior activations" is not implementable here — deferral stays
|
|
202
|
-
// consistently ON and a resumed conversation re-discovers via search_tools (documented divergence
|
|
203
|
-
// from serving, where activations persist in the session).
|
|
204
|
-
const deferredNames = customTools.filter(isDeferredTool).map((t) => t.name);
|
|
205
|
-
if (deferredNames.length > 0) {
|
|
206
|
-
const active = result.session.getActiveToolNames();
|
|
207
|
-
if (deferredNames.some((n) => active.includes(n))) {
|
|
208
|
-
result.session.setActiveToolsByName(active.filter((n) => !deferredNames.includes(n)));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
212
|
-
};
|
|
213
|
-
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
214
|
-
cwd: rootCwd,
|
|
215
|
-
agentDir: getAgentDir(),
|
|
216
|
-
sessionManager: sessionManager ?? SessionManager.create(rootCwd),
|
|
217
|
-
});
|
|
218
|
-
enforceWorkspaceScopedSessionSwitches(runtime, rootCwd);
|
|
219
|
-
return runtime;
|
|
220
|
-
}
|
|
221
|
-
function workspaceScopeError(targetCwd) {
|
|
222
|
-
return new Error(`fastagent chat is workspace-scoped: cannot switch to ${targetCwd}; run \`fastagent chat ${targetCwd}\` instead`);
|
|
223
|
-
}
|
|
224
|
-
function readSessionHeaderCwd(sessionPath) {
|
|
225
|
-
const resolvedPath = resolve(sessionPath);
|
|
226
|
-
if (!existsSync(resolvedPath))
|
|
227
|
-
return undefined;
|
|
228
|
-
for (const line of readFileSync(resolvedPath, "utf8").split(/\r?\n/)) {
|
|
229
|
-
if (!line.trim())
|
|
230
|
-
continue;
|
|
231
|
-
try {
|
|
232
|
-
const entry = JSON.parse(line);
|
|
233
|
-
if (entry.type === "session")
|
|
234
|
-
return typeof entry.cwd === "string" ? canonicalPath(entry.cwd) : undefined;
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// Ignore malformed lines the same way pi's session loader does; no header cwd → caller pins root.
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Keep resume/import inside the chat's single workspace, deciding BEFORE delegating to pi. The chat
|
|
244
|
-
* process is chdir'd into rootCwd, so a session with no cwd header already lands on rootCwd. The gap
|
|
245
|
-
* is a session that EXPLICITLY records a different cwd: pi would bind it and the factory would reject
|
|
246
|
-
* it — but only AFTER tearing the live session down. Reject such a switch up front.
|
|
6
|
+
* The TUI is ONE consumer of the shared definition-aware builder (session-builder.ts) — the same
|
|
7
|
+
* assembly, model surface, and fastagent auth that serving uses. Log in with `fastagent login` (or
|
|
8
|
+
* pi's TUI `/login`, which writes through the same credential store into the workspace auth file).
|
|
247
9
|
*/
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const target = cwdOverride !== undefined ? canonicalPath(cwdOverride) : readSessionHeaderCwd(sessionPath);
|
|
251
|
-
if (target !== undefined && target !== rootCwd)
|
|
252
|
-
throw workspaceScopeError(target);
|
|
253
|
-
};
|
|
254
|
-
const switchSession = runtime.switchSession.bind(runtime);
|
|
255
|
-
runtime.switchSession = async (...args) => {
|
|
256
|
-
rejectForeignTarget(args[0], args[1]?.cwdOverride);
|
|
257
|
-
return switchSession(...args);
|
|
258
|
-
};
|
|
259
|
-
const importFromJsonl = runtime.importFromJsonl.bind(runtime);
|
|
260
|
-
runtime.importFromJsonl = async (...args) => {
|
|
261
|
-
rejectForeignTarget(args[0], args[1]);
|
|
262
|
-
return importFromJsonl(...args);
|
|
263
|
-
};
|
|
264
|
-
}
|
|
10
|
+
import { InteractiveMode } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { buildAgentSessionRuntime } from "./session-builder.js";
|
|
265
12
|
/**
|
|
266
13
|
* Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
|
|
267
|
-
* fastagent's assembled agent (same model/tools/skills/prompt as dev/start serve); pi's TUI
|
|
268
|
-
* login, rendering, and same-workspace sessions natively.
|
|
14
|
+
* fastagent's assembled agent (same model/tools/skills/prompt/auth as dev/start serve); pi's TUI
|
|
15
|
+
* handles login, rendering, and same-workspace sessions natively.
|
|
269
16
|
*/
|
|
270
17
|
export async function runPiChat(dir, options = {}) {
|
|
271
|
-
const runtime = await
|
|
18
|
+
const runtime = await buildAgentSessionRuntime(dir, options);
|
|
272
19
|
await new InteractiveMode(runtime, {}).run();
|
|
273
20
|
}
|