@fastagent-sh/fastagent 0.12.1 → 0.14.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 +6 -4
- package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
- package/dist/channels/feishu/bootstrap-token.js +94 -0
- package/dist/channels/feishu/card.d.ts +32 -0
- package/dist/channels/feishu/card.js +66 -0
- package/dist/channels/feishu/cloud.d.ts +17 -0
- package/dist/channels/feishu/cloud.js +19 -0
- package/dist/channels/feishu/context-buffer.d.ts +46 -0
- package/dist/channels/feishu/context-buffer.js +133 -0
- package/dist/channels/feishu/crypto.d.ts +15 -0
- package/dist/channels/feishu/crypto.js +43 -0
- package/dist/channels/feishu/feishu-api.d.ts +108 -0
- package/dist/channels/feishu/feishu-api.js +325 -0
- package/dist/channels/feishu/feishu.d.ts +46 -0
- package/dist/channels/feishu/feishu.js +472 -0
- package/dist/channels/feishu/invoke-turn.d.ts +65 -0
- package/dist/channels/feishu/invoke-turn.js +157 -0
- package/dist/channels/feishu/model.d.ts +97 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +22 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +11 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +43 -0
- package/dist/channels/feishu/parse.js +65 -0
- package/dist/channels/feishu/preview.d.ts +36 -0
- package/dist/channels/feishu/preview.js +387 -0
- package/dist/channels/feishu/register-app.d.ts +70 -0
- package/dist/channels/feishu/register-app.js +141 -0
- package/dist/channels/feishu/register-webhook.d.ts +22 -0
- package/dist/channels/feishu/register-webhook.js +106 -0
- package/dist/channels/feishu/scaffold/channel.ts +41 -0
- package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
- package/dist/channels/feishu/seen.d.ts +5 -0
- package/dist/channels/feishu/seen.js +47 -0
- package/dist/channels/feishu/text.d.ts +13 -0
- package/dist/channels/feishu/text.js +63 -0
- package/dist/channels/lark/lark.d.ts +15 -0
- package/dist/channels/lark/lark.js +10 -0
- package/dist/channels/lark/onboard.d.ts +39 -0
- package/dist/channels/lark/onboard.js +58 -0
- package/dist/channels/lark/scaffold/channel.ts +39 -0
- package/dist/channels/lark/scaffold/lark-send.ts +87 -0
- package/dist/channels/registration.d.ts +15 -0
- package/dist/channels/registration.js +1 -0
- package/dist/channels/{telegram/state.js → state.js} +6 -4
- package/dist/channels/telegram/context-buffer.js +1 -1
- package/dist/channels/telegram/register-webhook.d.ts +4 -1
- package/dist/channels/telegram/register-webhook.js +17 -26
- package/dist/channels/telegram/telegram.js +2 -2
- package/dist/channels/telegram/turn-store.d.ts +8 -21
- package/dist/channels/telegram/turn-store.js +11 -130
- package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
- package/dist/channels/turn-store.d.ts +42 -0
- package/dist/channels/turn-store.js +139 -0
- package/dist/channels/wait-health.d.ts +6 -0
- package/dist/channels/wait-health.js +27 -0
- package/dist/cli/commands/add.d.ts +9 -0
- package/dist/cli/commands/add.js +142 -0
- package/dist/cli/commands/chat.d.ts +3 -0
- package/dist/cli/commands/chat.js +16 -0
- package/dist/cli/commands/deploy.d.ts +13 -0
- package/dist/cli/commands/deploy.js +338 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +76 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +29 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +63 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +90 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +37 -0
- package/dist/cli/fail.d.ts +19 -0
- package/dist/cli/fail.js +32 -0
- package/dist/cli/kernel.d.ts +89 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +421 -0
- package/dist/cli/serve.d.ts +28 -0
- package/dist/cli/serve.js +90 -0
- package/dist/cli/shared.d.ts +24 -0
- package/dist/cli/shared.js +116 -0
- package/dist/cli-add-feishu.d.ts +8 -0
- package/dist/cli-add-feishu.js +223 -0
- package/dist/cli.js +8 -1284
- package/dist/deploy/container.js +10 -6
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +139 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +1 -1
- package/dist/deploy/fly/plan.js +15 -4
- package/dist/deploy/fly/run.d.ts +7 -4
- package/dist/deploy/fly/run.js +26 -5
- package/dist/deploy/preflight.js +4 -3
- package/dist/deploy/railway/plan.d.ts +1 -1
- package/dist/deploy/railway/plan.js +17 -5
- package/dist/deploy/railway/run.d.ts +6 -3
- package/dist/deploy/railway/run.js +26 -4
- package/dist/deploy/registration-gate.d.ts +20 -0
- package/dist/deploy/registration-gate.js +20 -0
- package/dist/deploy/runner.d.ts +4 -1
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +10 -9
- package/dist/deploy/secrets.js +15 -14
- package/dist/dev-supervisor.js +2 -1
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/chat.js +80 -7
- package/dist/engines/pi/config.d.ts +12 -3
- package/dist/engines/pi/config.js +16 -1
- package/dist/engines/pi/create.d.ts +14 -6
- package/dist/engines/pi/create.js +52 -16
- package/dist/engines/pi/harness.d.ts +16 -1
- package/dist/engines/pi/harness.js +77 -1
- package/dist/engines/pi/invoke.d.ts +1 -1
- package/dist/engines/pi/invoke.js +37 -2
- package/dist/engines/pi/login.js +1 -1
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/tool-context.d.ts +28 -0
- package/dist/engines/pi/tool-context.js +8 -0
- package/dist/engines/pi/tool.d.ts +32 -1
- package/dist/engines/pi/tool.js +42 -1
- package/dist/engines/pi/workspace.d.ts +4 -1
- package/dist/engines/pi/workspace.js +3 -1
- package/dist/feishu.d.ts +2 -0
- package/dist/feishu.js +2 -0
- package/dist/lark.d.ts +3 -0
- package/dist/lark.js +3 -0
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +6 -0
- package/dist/pi.d.ts +2 -1
- package/dist/scaffold/add-channel.d.ts +9 -5
- package/dist/scaffold/add-channel.js +75 -7
- package/dist/scaffold/templates/fastagent.config.mjs +1 -0
- package/dist/tunnel.d.ts +9 -6
- package/dist/tunnel.js +48 -31
- package/package.json +18 -4
- /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
- /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { FastagentTool } from "./tool.ts";
|
|
2
3
|
import type { Models } from "@earendil-works/pi-ai";
|
|
3
4
|
import type { AnyModel } from "./harness.ts";
|
|
5
|
+
/** pi's thinking levels (types.d.ts `ThinkingLevel`), as a runtime list for config validation — pi
|
|
6
|
+
* exports only the type. A level the selected model does not support is clamped by pi per model. */
|
|
7
|
+
export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
4
8
|
export interface FastagentConfig {
|
|
5
9
|
/** "provider/modelId". Precedence: CLI --model > FASTAGENT_MODEL > config. */
|
|
6
10
|
model?: string;
|
|
11
|
+
/** Reasoning effort for the model, pi's scale ("off" | "minimal" | "low" | "medium" | "high" |
|
|
12
|
+
* "xhigh" | "max"). Unset = pi's default. Authors tune thinking in the pi TUI while vibing — this
|
|
13
|
+
* is the serving-side counterpart (fidelity). Levels a model doesn't support are clamped by pi. */
|
|
14
|
+
thinkingLevel?: ThinkingLevel;
|
|
7
15
|
/**
|
|
8
16
|
* The agent-definition subdirectory (persona.md, skills/, tools/, channels/), relative to the config
|
|
9
17
|
* file's directory. Default: the config directory itself (flat — today's behaviour). Point it at a
|
|
@@ -12,8 +20,9 @@ export interface FastagentConfig {
|
|
|
12
20
|
* subdir and does not collide with the host's `tools/`/`src/` (core.md scenario grid).
|
|
13
21
|
*/
|
|
14
22
|
agentDir?: string;
|
|
15
|
-
/** Extra custom tools, appended after pi defaults — never replaces them.
|
|
16
|
-
|
|
23
|
+
/** Extra custom tools, appended after pi defaults — never replaces them. `FastagentTool` = AgentTool
|
|
24
|
+
* plus the optional `deferred` marker (see defineTool). */
|
|
25
|
+
tools?: FastagentTool[];
|
|
17
26
|
http?: {
|
|
18
27
|
port?: number;
|
|
19
28
|
};
|
|
@@ -19,6 +19,17 @@ import { homedir } from "node:os";
|
|
|
19
19
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import { moduleLoadHint } from "../../loader.js";
|
|
22
|
+
/** pi's thinking levels (types.d.ts `ThinkingLevel`), as a runtime list for config validation — pi
|
|
23
|
+
* exports only the type. A level the selected model does not support is clamped by pi per model. */
|
|
24
|
+
export const THINKING_LEVELS = [
|
|
25
|
+
"off",
|
|
26
|
+
"minimal",
|
|
27
|
+
"low",
|
|
28
|
+
"medium",
|
|
29
|
+
"high",
|
|
30
|
+
"xhigh",
|
|
31
|
+
"max",
|
|
32
|
+
];
|
|
22
33
|
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
23
34
|
export function defineConfig(config) {
|
|
24
35
|
return config;
|
|
@@ -70,17 +81,21 @@ export async function loadConfig(dir) {
|
|
|
70
81
|
// (`modle:`) must not silently degrade to zero-config.
|
|
71
82
|
for (const key of Object.keys(c)) {
|
|
72
83
|
if (key !== "model" &&
|
|
84
|
+
key !== "thinkingLevel" &&
|
|
73
85
|
key !== "agentDir" &&
|
|
74
86
|
key !== "tools" &&
|
|
75
87
|
key !== "http" &&
|
|
76
88
|
key !== "deploy" &&
|
|
77
89
|
key !== "selfSchedule") {
|
|
78
|
-
throw new Error(`${path}: unknown key "${key}" (valid keys: model, agentDir, tools, http, deploy, selfSchedule)`);
|
|
90
|
+
throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, agentDir, tools, http, deploy, selfSchedule)`);
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
if (c.model !== undefined && typeof c.model !== "string") {
|
|
82
94
|
throw new Error(`${path}: "model" must be a "provider/modelId" string`);
|
|
83
95
|
}
|
|
96
|
+
if (c.thinkingLevel !== undefined && !THINKING_LEVELS.includes(c.thinkingLevel)) {
|
|
97
|
+
throw new Error(`${path}: "thinkingLevel" must be one of ${THINKING_LEVELS.join(", ")}`);
|
|
98
|
+
}
|
|
84
99
|
if (c.agentDir !== undefined && typeof c.agentDir !== "string") {
|
|
85
100
|
throw new Error(`${path}: "agentDir" must be a string (a subdirectory relative to the config file)`);
|
|
86
101
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AgentTool, ExecutionEnv, Skill } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Agent } from "../../agent.ts";
|
|
4
4
|
import { type FastagentConfig } from "./config.ts";
|
|
5
5
|
import { type LoadedDefinition } from "./definition.ts";
|
|
6
6
|
import { type PiSessionStore } from "./sessions.ts";
|
|
7
7
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
8
|
-
import { type ToolCollision } from "./tool.ts";
|
|
8
|
+
import { type FastagentTool, type ToolCollision } from "./tool.ts";
|
|
9
9
|
import { type Lease } from "./invoke.ts";
|
|
10
10
|
/** pi's core default toolset (read/bash/edit/write), rooted at cwd. */
|
|
11
11
|
export declare function piDefaultTools(cwd: string): AgentTool[];
|
|
@@ -19,6 +19,9 @@ export declare function resolveTools(config: FastagentConfig, cwd: string): Agen
|
|
|
19
19
|
export declare function resolveWorkspaceTools(config: FastagentConfig, agentDir: string, cwd?: string): Promise<{
|
|
20
20
|
tools: AgentTool[];
|
|
21
21
|
toolNames: string[];
|
|
22
|
+
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
23
|
+
* via the built-in `search_tools` loader. Surfaced so the operator can see deferral took effect. */
|
|
24
|
+
deferredToolNames: string[];
|
|
22
25
|
toolCollisions: ToolCollision[];
|
|
23
26
|
toolFailures: ModuleLoadFailure[];
|
|
24
27
|
}>;
|
|
@@ -46,7 +49,6 @@ export interface AssembleSystemPromptOptions {
|
|
|
46
49
|
/** ③ Skills for the <available_skills> listing. */
|
|
47
50
|
skills?: Skill[];
|
|
48
51
|
/** ④ Env context, caller-provided (keeps this function pure). Omitted = segment omitted. */
|
|
49
|
-
date?: string;
|
|
50
52
|
cwd?: string;
|
|
51
53
|
}
|
|
52
54
|
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
@@ -54,13 +56,16 @@ export declare function assembleSystemPrompt(options: AssembleSystemPromptOption
|
|
|
54
56
|
export interface CreatePiAgentOptions {
|
|
55
57
|
/** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
|
|
56
58
|
model: string;
|
|
59
|
+
/** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
|
|
60
|
+
thinkingLevel?: ThinkingLevel;
|
|
57
61
|
/**
|
|
58
62
|
* The system prompt itself — verbatim, no engine base and no wrapping (unlike the directory path,
|
|
59
63
|
* which assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string
|
|
60
64
|
* or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
61
65
|
*/
|
|
62
66
|
instructions?: string | (() => string);
|
|
63
|
-
|
|
67
|
+
/** `FastagentTool` = AgentTool plus the optional `deferred` marker (see {@link DefineToolOptions}). */
|
|
68
|
+
tools?: FastagentTool[];
|
|
64
69
|
skills?: Skill[];
|
|
65
70
|
/**
|
|
66
71
|
* Extra providers registered on top of the built-ins — your own gateway / self-hosted endpoint /
|
|
@@ -91,11 +96,14 @@ export declare function createPiAgent(options: CreatePiAgentOptions): Agent;
|
|
|
91
96
|
export interface CreatePiAgentFromDefinitionOptions {
|
|
92
97
|
/** Model spec "provider/modelId", resolved against {@link models}. */
|
|
93
98
|
model: string;
|
|
99
|
+
/** Reasoning effort (pi's scale). Unset = pi's default; unsupported levels are clamped per model. */
|
|
100
|
+
thinkingLevel?: ThinkingLevel;
|
|
94
101
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
95
102
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
96
103
|
base?: string;
|
|
97
|
-
/** Override tools. Defaults to piDefaultTools (lock down with a custom list).
|
|
98
|
-
|
|
104
|
+
/** Override tools. Defaults to piDefaultTools (lock down with a custom list). `FastagentTool` =
|
|
105
|
+
* AgentTool plus the optional `deferred` marker. */
|
|
106
|
+
tools?: FastagentTool[];
|
|
99
107
|
/**
|
|
100
108
|
* The agent's working directory: where the default tools operate AND whose ancestors are walked for
|
|
101
109
|
* ② project context (AGENTS.md). Defaults to `dir` (flat: the definition dir is also the run root).
|
|
@@ -19,7 +19,8 @@ import { piHarnessFactory } from "./harness.js";
|
|
|
19
19
|
import { createPiModels } from "./models.js";
|
|
20
20
|
import { reportDefinitionWarnings } from "./report.js";
|
|
21
21
|
import { inMemorySessionStore } from "./sessions.js";
|
|
22
|
-
import { loadTools, mergeDiscoveredTools } from "./tool.js";
|
|
22
|
+
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
23
|
+
import { withSearchTool } from "./search-tools.js";
|
|
23
24
|
import { createPiAgentFromHarness } from "./invoke.js";
|
|
24
25
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
25
26
|
//
|
|
@@ -44,20 +45,43 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
44
45
|
// Default coding tools (read/bash/edit/write) are rooted at `cwd` (the run root the agent operates on);
|
|
45
46
|
// discovered `tools/` come from `agentDir` (the agent's own surface). They coincide in the flat case.
|
|
46
47
|
const discovered = await loadTools(agentDir);
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
const merged = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
|
|
49
|
+
// The built-in `search_tools` loader mounts here — the one place the workspace's full tool set is
|
|
50
|
+
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; a
|
|
51
|
+
// workspace-defined search_tools wins).
|
|
52
|
+
const tools = withSearchTool(merged.tools);
|
|
53
|
+
// Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
|
|
54
|
+
// on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
|
|
55
|
+
const builtinLoaderMounted = !merged.tools.some((t) => t.name === "search_tools") && tools.some((t) => t.name === "search_tools");
|
|
56
|
+
const toolCollisions = [...discovered.collisions, ...merged.collisions];
|
|
57
|
+
// `toolNames` is the AUTHOR's active-by-default surface (config.tools + tools/): exclude pi
|
|
58
|
+
// defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
|
|
59
|
+
// slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools —
|
|
60
|
+
// each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
49
61
|
const defaultNames = new Set(piDefaultTools(cwd).map((t) => t.name));
|
|
50
|
-
const toolNames = tools
|
|
51
|
-
|
|
62
|
+
const toolNames = tools
|
|
63
|
+
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
64
|
+
.map((t) => t.name);
|
|
65
|
+
return {
|
|
66
|
+
tools,
|
|
67
|
+
toolNames,
|
|
68
|
+
deferredToolNames: tools.filter(isDeferredTool).map((t) => t.name),
|
|
69
|
+
toolCollisions,
|
|
70
|
+
toolFailures: discovered.failures,
|
|
71
|
+
};
|
|
52
72
|
}
|
|
53
73
|
// ── §2 prompt: four-segment systemPrompt assembly ───────────────────────────
|
|
54
74
|
//
|
|
55
75
|
// systemPrompt = ① base (engine asset; a persona.md persona overrides its identity line)
|
|
56
76
|
// + ② project context (AGENTS.md files via pi's loadProjectContextFiles, <project_context>-wrapped)
|
|
57
|
-
// + ③ skills listing + ④ env context (
|
|
77
|
+
// + ③ skills listing + ④ env context (cwd)
|
|
58
78
|
//
|
|
59
|
-
// AGENTS.md ≠ system prompt. Pure functions: segment ④
|
|
60
|
-
// same inputs always produce the same prompt (testable, reproducible).
|
|
79
|
+
// AGENTS.md ≠ system prompt. Pure functions: segment ④ input (cwd) is caller-provided, so the
|
|
80
|
+
// same inputs always produce the same prompt (testable, reproducible). No date: a date line would
|
|
81
|
+
// invalidate the provider prompt cache (a prefix cache) for every session at each day boundary —
|
|
82
|
+
// channel sessions routinely live for weeks (pi ≥0.80.7 dropped it from its default prompt for the
|
|
83
|
+
// same reason). The model gets the date when it needs it: `bash date`, and the wake tool takes
|
|
84
|
+
// relative delays ("30m") / cron — never an absolute now-derived instant.
|
|
61
85
|
/**
|
|
62
86
|
* The pi engine's base prompt (segment ①), mirroring pi-coding-agent's default path with two
|
|
63
87
|
* deviations: the pi-TUI docs section is dropped (those paths don't exist in deployments), and the
|
|
@@ -65,16 +89,25 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
65
89
|
* `persona` (from persona.md) replaces the default identity line, keeping the tools list + guidelines.
|
|
66
90
|
*/
|
|
67
91
|
export function piBasePrompt(options = {}) {
|
|
68
|
-
const
|
|
92
|
+
const mounted = options.tools ?? [];
|
|
93
|
+
// Deferred tools stay OUT of the list: their schemas are not in the request until activated, so
|
|
94
|
+
// naming them here would invite calls to tools that don't exist yet; discovery is search_tools' job
|
|
95
|
+
// (which IS listed — it's active). Computed from the static mounted set, so the prompt — the cached
|
|
96
|
+
// context prefix — does not change when a tool is activated mid-session.
|
|
97
|
+
const tools = mounted.filter((t) => !isDeferredTool(t));
|
|
98
|
+
const deferredCount = mounted.length - tools.length;
|
|
69
99
|
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
|
|
70
100
|
// Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
|
|
71
101
|
// (the standalone×code-repo cell's persona; core.md §11), keeping the tools list + guidelines below.
|
|
72
102
|
const identity = options.persona?.trim() ||
|
|
73
103
|
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
104
|
+
const deferredNote = deferredCount > 0
|
|
105
|
+
? `\n\n${deferredCount} additional tool(s) are registered but inactive — use search_tools to discover and activate them before concluding a capability is missing.`
|
|
106
|
+
: "";
|
|
74
107
|
return `${identity}
|
|
75
108
|
|
|
76
109
|
Available tools:
|
|
77
|
-
${toolsList}
|
|
110
|
+
${toolsList}${deferredNote}
|
|
78
111
|
|
|
79
112
|
In addition to the tools above, you may have access to other custom tools depending on the project.
|
|
80
113
|
|
|
@@ -96,8 +129,6 @@ export function assembleSystemPrompt(options) {
|
|
|
96
129
|
if (options.skills && options.skills.length > 0) {
|
|
97
130
|
prompt += `\n${formatSkillsForSystemPrompt(options.skills)}\n`;
|
|
98
131
|
}
|
|
99
|
-
if (options.date)
|
|
100
|
-
prompt += `\nCurrent date: ${options.date}`;
|
|
101
132
|
if (options.cwd)
|
|
102
133
|
prompt += `\nCurrent working directory: ${options.cwd}`;
|
|
103
134
|
return prompt;
|
|
@@ -117,6 +148,7 @@ function buildPiAgent(opts) {
|
|
|
117
148
|
env: opts.env ?? new NodeExecutionEnv({ cwd: process.cwd() }),
|
|
118
149
|
models,
|
|
119
150
|
model: resolveModel(models, opts.model),
|
|
151
|
+
thinkingLevel: opts.thinkingLevel,
|
|
120
152
|
systemPrompt: opts.systemPrompt,
|
|
121
153
|
tools: opts.tools,
|
|
122
154
|
skills: opts.skills,
|
|
@@ -143,10 +175,12 @@ function instructionsPrompt(instructions, skills) {
|
|
|
143
175
|
export function createPiAgent(options) {
|
|
144
176
|
return buildPiAgent({
|
|
145
177
|
model: options.model,
|
|
178
|
+
thinkingLevel: options.thinkingLevel,
|
|
146
179
|
providers: options.providers,
|
|
147
180
|
authPath: options.authPath,
|
|
148
181
|
systemPrompt: instructionsPrompt(options.instructions, options.skills),
|
|
149
|
-
tools
|
|
182
|
+
// Deferred tools need their loader on every rung (idempotent; the caller's own search_tools wins).
|
|
183
|
+
tools: options.tools ? withSearchTool(options.tools) : options.tools,
|
|
150
184
|
skills: options.skills,
|
|
151
185
|
sessions: options.sessions,
|
|
152
186
|
env: options.env,
|
|
@@ -175,9 +209,12 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
175
209
|
// runtime-written bad skill surfaces the moment it appears, while a static finding does not spam
|
|
176
210
|
// every turn's log. A log-dedup memo, not session state (stateless invoke holds).
|
|
177
211
|
let reportedFindings = findingsSignature(definition);
|
|
178
|
-
|
|
212
|
+
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
213
|
+
// it; a caller's own search_tools wins).
|
|
214
|
+
const tools = withSearchTool(options.tools ?? piDefaultTools(env.cwd));
|
|
179
215
|
const agent = buildPiAgent({
|
|
180
216
|
model: options.model,
|
|
217
|
+
thinkingLevel: options.thinkingLevel,
|
|
181
218
|
providers: options.providers,
|
|
182
219
|
// Dir-aware default: the same state-root-derived file the opener uses for this dir (the opener
|
|
183
220
|
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
@@ -185,7 +222,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
185
222
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
|
|
186
223
|
// edits (the author's, or the agent's own self-modification) take effect on the next turn with
|
|
187
224
|
// no process restart — restarts are reserved for code (tools/channels/config, module cache).
|
|
188
|
-
// One read yields prompt AND skills (they can never diverge),
|
|
225
|
+
// One read yields prompt AND skills (they can never diverge), and the
|
|
189
226
|
// fs cost is a few reads against a model call. Broken edits stay visible: a throw-class problem
|
|
190
227
|
// (unreadable AGENTS.md) fails that turn's invoke, and the loader's NON-fatal findings (bad
|
|
191
228
|
// SKILL.md frontmatter, name collisions — returned as data, not thrown) are warned the moment
|
|
@@ -207,7 +244,6 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
207
244
|
// ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
|
|
208
245
|
contextFiles: def.contextFiles,
|
|
209
246
|
skills: def.skills,
|
|
210
|
-
date: new Date().toISOString().slice(0, 10),
|
|
211
247
|
cwd: env.cwd,
|
|
212
248
|
}),
|
|
213
249
|
skills: def.skills,
|
|
@@ -7,9 +7,20 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
-
import type { AgentTool, ExecutionEnv, Skill } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import type { AgentTool, ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
11
11
|
import type { Model, Models } from "@earendil-works/pi-ai";
|
|
12
12
|
import type { PiSessionStore } from "./sessions.ts";
|
|
13
|
+
/**
|
|
14
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
15
|
+
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
16
|
+
* `active_tools_change` entries are full active-set SNAPSHOTS (setActiveTools persists everything
|
|
17
|
+
* active at that moment), and reinterpreting a snapshot as activations would keep a tool active in
|
|
18
|
+
* old sessions after the author flips it to `deferred` — the session never discovered it. Deltas
|
|
19
|
+
* carry only what was actually discovered.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
22
|
+
export type PiSession = Awaited<ReturnType<PiSessionStore["openOrCreate"]>>;
|
|
23
|
+
export declare function harnessSession(harness: AgentHarness): PiSession | undefined;
|
|
13
24
|
/**
|
|
14
25
|
* pi's Model with the API-shape generic erased — fastagent only passes models through to the
|
|
15
26
|
* harness, so the generic carries no information. One alias keeps the `any` auditable.
|
|
@@ -24,6 +35,9 @@ export interface PiHarnessFactoryOptions {
|
|
|
24
35
|
/** Provider collection for all model requests; {@link model} must belong to it (same provider id). */
|
|
25
36
|
models: Models;
|
|
26
37
|
model: AnyModel;
|
|
38
|
+
/** Reasoning effort for the model (pi's scale). Unset = fastagent's pinned default ("medium", pi
|
|
39
|
+
* TUI parity — see {@link DEFAULT_THINKING_LEVEL}); unsupported levels are clamped by pi per model. */
|
|
40
|
+
thinkingLevel?: ThinkingLevel;
|
|
27
41
|
tools?: AgentTool[];
|
|
28
42
|
/**
|
|
29
43
|
* Final assembled prompt, or a SYNC factory re-evaluated per invoke (how L1 serves dynamic
|
|
@@ -46,5 +60,6 @@ export interface PiHarnessFactoryOptions {
|
|
|
46
60
|
skills?: Skill[];
|
|
47
61
|
}>;
|
|
48
62
|
}
|
|
63
|
+
export declare function resolveHarnessActiveToolNames(recorded: string[] | null, tools: AgentTool[], sessionId: string): string[] | undefined;
|
|
49
64
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
50
65
|
export declare function piHarnessFactory(options: PiHarnessFactoryOptions): PiHarnessFactory;
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
* historical entries back into context via buildContext().
|
|
8
8
|
*/
|
|
9
9
|
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import { log } from "../../log.js";
|
|
11
|
+
import { isDeferredTool } from "./tool.js";
|
|
12
|
+
/**
|
|
13
|
+
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
14
|
+
* tools a loader activated in that call. The DEDICATED record the resolve below reads: pi's own
|
|
15
|
+
* `active_tools_change` entries are full active-set SNAPSHOTS (setActiveTools persists everything
|
|
16
|
+
* active at that moment), and reinterpreting a snapshot as activations would keep a tool active in
|
|
17
|
+
* old sessions after the author flips it to `deferred` — the session never discovered it. Deltas
|
|
18
|
+
* carry only what was actually discovered.
|
|
19
|
+
*/
|
|
20
|
+
export const TOOL_ACTIVATION_ENTRY = "fastagent:tool-activation";
|
|
21
|
+
/** The session a factory-built harness is bound to — the seam the activation bridge (invoke.ts) uses
|
|
22
|
+
* to write {@link TOOL_ACTIVATION_ENTRY} deltas (pi's harness keeps its session private). Absent for
|
|
23
|
+
* a harness built outside {@link piHarnessFactory}: activation still works in-turn there, but is not
|
|
24
|
+
* recorded — the factory owns persistence. */
|
|
25
|
+
const harnessSessions = new WeakMap();
|
|
26
|
+
export function harnessSession(harness) {
|
|
27
|
+
return harnessSessions.get(harness);
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* Provider request retries. The OpenAI-family / Anthropic / Azure / Codex pi-ai adapters
|
|
12
31
|
* implement client-side retries (429/5xx/request-phase network failures with backoff, honoring
|
|
@@ -19,23 +38,80 @@ import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
|
19
38
|
* so a mid-stream failure surfaces as a `failed` event.
|
|
20
39
|
*/
|
|
21
40
|
const PROVIDER_MAX_RETRIES = 2;
|
|
41
|
+
/**
|
|
42
|
+
* The serving default for reasoning effort, pinned to what pi's TUI defaults to (its
|
|
43
|
+
* DEFAULT_THINKING_LEVEL) — NOT inherited from the bare harness, whose own fallback is "off": an
|
|
44
|
+
* author vibes at "medium" in pi and must get "medium" when served (fidelity), and pinning the value
|
|
45
|
+
* here means an upstream default change in either place cannot silently alter deployments. Models
|
|
46
|
+
* that don't support a level are clamped by pi per model.
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_THINKING_LEVEL = "medium";
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the active-tool set for a fresh harness — the ONE place both fallbacks live. pi's harness
|
|
51
|
+
* WRITES active-tool changes to the session (`setActiveTools` → `active_tools_change`) but its
|
|
52
|
+
* constructor never reads them back — pi's long-lived TUI harness keeps the set in memory, while
|
|
53
|
+
* fastagent builds a FRESH harness per invoke, which would silently reset the session's active set
|
|
54
|
+
* every turn.
|
|
55
|
+
*
|
|
56
|
+
* No record (`null`) → the INITIAL set: every non-deferred tool; undefined when nothing is deferred
|
|
57
|
+
* (pi's default — all active — applies, and no session entry is ever written; tool-sets without
|
|
58
|
+
* deferral behave exactly as before deferral existed).
|
|
59
|
+
*
|
|
60
|
+
* A record is NOT replayed as a frozen snapshot — the active set is rebuilt as the UNION of the
|
|
61
|
+
* initial set and the recorded names (filtered to the mounted tools: the constructor THROWS on
|
|
62
|
+
* unknown names, so a recorded-but-removed tool would otherwise brick every future invoke of that
|
|
63
|
+
* session). On the serving path only the additive activation bridge writes records, so a record's
|
|
64
|
+
* real semantic is "which deferred tools this session activated" — layered on top of whatever the
|
|
65
|
+
* workspace mounts TODAY. A snapshot replay would silently freeze a later-added non-deferred tool
|
|
66
|
+
* out of every session the loader ever touched. Missing recorded names are logged (fail visibly) —
|
|
67
|
+
* ONCE per session+missing set: a fresh harness is built per invoke and channel sessions live for
|
|
68
|
+
* weeks, so an un-deduped warn would repeat every turn and dilute its own signal. A log-dedup memo
|
|
69
|
+
* (like L2's findings memo), not session state — the resolve stays derived from the session.
|
|
70
|
+
*/
|
|
71
|
+
const warnedRestores = new Set();
|
|
72
|
+
export function resolveHarnessActiveToolNames(recorded, tools, sessionId) {
|
|
73
|
+
const anyDeferred = tools.some(isDeferredTool);
|
|
74
|
+
const initial = tools.filter((t) => !isDeferredTool(t)).map((t) => t.name);
|
|
75
|
+
if (recorded === null)
|
|
76
|
+
return anyDeferred ? initial : undefined;
|
|
77
|
+
const mounted = new Set(tools.map((t) => t.name));
|
|
78
|
+
const known = recorded.filter((name) => mounted.has(name));
|
|
79
|
+
const missing = recorded.filter((name) => !mounted.has(name));
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
const emit = warnedRestores.has(`${sessionId}\u0000${missing.join(",")}`) ? log.debug : log.warn;
|
|
82
|
+
warnedRestores.add(`${sessionId}\u0000${missing.join(",")}`);
|
|
83
|
+
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${missing.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
return [...new Set([...initial, ...known])];
|
|
86
|
+
}
|
|
22
87
|
/** Open-or-create the session per invoke: existing → open (history via buildContext); missing → create. */
|
|
23
88
|
export function piHarnessFactory(options) {
|
|
24
89
|
return async (sessionId) => {
|
|
25
90
|
const session = await options.sessions.openOrCreate(sessionId);
|
|
91
|
+
// One extra entry walk per invoke to collect the activation deltas — negligible against the model
|
|
92
|
+
// call, same trade as L2's per-invoke definition re-read. Serving sessions never branch, so a flat
|
|
93
|
+
// getEntries() read (no leaf-path walk) is correct.
|
|
94
|
+
const entries = await session.getEntries();
|
|
95
|
+
const activated = entries.flatMap((e) => e.type === "custom" && e.customType === TOOL_ACTIVATION_ENTRY
|
|
96
|
+
? (e.data?.names ?? [])
|
|
97
|
+
: []);
|
|
26
98
|
const fresh = options.live ? await options.live() : undefined;
|
|
27
99
|
const { systemPrompt } = options;
|
|
28
100
|
const prompt = fresh ? fresh.systemPrompt : typeof systemPrompt === "function" ? systemPrompt() : systemPrompt;
|
|
29
101
|
const skills = fresh ? fresh.skills : options.skills;
|
|
30
|
-
|
|
102
|
+
const harness = new AgentHarness({
|
|
31
103
|
env: options.env,
|
|
32
104
|
session,
|
|
33
105
|
models: options.models,
|
|
34
106
|
model: options.model,
|
|
107
|
+
thinkingLevel: options.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
35
108
|
tools: options.tools,
|
|
109
|
+
activeToolNames: resolveHarnessActiveToolNames(activated.length > 0 ? activated : null, options.tools ?? [], sessionId),
|
|
36
110
|
systemPrompt: prompt,
|
|
37
111
|
resources: skills ? { skills } : undefined,
|
|
38
112
|
streamOptions: { maxRetries: PROVIDER_MAX_RETRIES },
|
|
39
113
|
});
|
|
114
|
+
harnessSessions.set(harness, session);
|
|
115
|
+
return harness;
|
|
40
116
|
};
|
|
41
117
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Agent, type AgentEvent } from "../../agent.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type PiHarnessFactory } from "./harness.ts";
|
|
4
4
|
export type Release = () => void;
|
|
5
5
|
export interface Lease {
|
|
6
6
|
/** Try to acquire exclusive write access for the session (fail-fast). Returns null if held. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_COMPACTION_SETTINGS, calculateContextTokens, shouldCompact } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
3
3
|
import { log } from "../../log.js";
|
|
4
|
-
import {
|
|
4
|
+
import { TOOL_ACTIVATION_ENTRY, harnessSession } from "./harness.js";
|
|
5
|
+
import { additiveActivation, turnContext } from "./tool-context.js";
|
|
5
6
|
export function inProcessLease() {
|
|
6
7
|
const busy = new Set();
|
|
7
8
|
return {
|
|
@@ -129,6 +130,40 @@ export function errorToTerminal(error) {
|
|
|
129
130
|
const details = error instanceof Error ? error.message : String(error);
|
|
130
131
|
return { type: "failed", details, retryable: classifyRetryable(details, errorSignal(error)) };
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The turn's {@link ToolActivation} over the live harness. `activate` is additive and filters to the
|
|
135
|
+
* registered names first — pi's `setActiveTools` THROWS on unknown names, and a loader must get a
|
|
136
|
+
* usable "nothing new" answer, not an exception. pi persists the change in the session, so the
|
|
137
|
+
* per-invoke restore (harness.ts) carries it into later turns.
|
|
138
|
+
*/
|
|
139
|
+
function toolActivation(harness) {
|
|
140
|
+
// Serialize activations per turn: "who activated first" must be decided HERE, not by whether pi's
|
|
141
|
+
// setActiveTools happens to mutate before its first await — parallel tool calls in one batch race
|
|
142
|
+
// their activate() calls, and the addedToolNames load points must not double-stamp.
|
|
143
|
+
let chain = Promise.resolve([]);
|
|
144
|
+
return {
|
|
145
|
+
active: () => harness.getActiveTools().map((t) => t.name),
|
|
146
|
+
registered: () => harness.getTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
147
|
+
activate(names) {
|
|
148
|
+
const run = async () => {
|
|
149
|
+
const current = harness.getActiveTools().map((t) => t.name);
|
|
150
|
+
const added = additiveActivation(harness.getTools().map((t) => t.name), current, names);
|
|
151
|
+
if (added.length > 0) {
|
|
152
|
+
await harness.setActiveTools([...current, ...added]);
|
|
153
|
+
// Persist the DELTA in a dedicated entry — what the per-invoke resolve (harness.ts) reads.
|
|
154
|
+
// pi's own active_tools_change record is a full snapshot and is deliberately ignored there.
|
|
155
|
+
// Absent session (a harness built outside piHarnessFactory): in-turn activation still works,
|
|
156
|
+
// it just isn't durable — the factory owns persistence.
|
|
157
|
+
await harnessSession(harness)?.appendCustomEntry(TOOL_ACTIVATION_ENTRY, { names: added });
|
|
158
|
+
}
|
|
159
|
+
return added;
|
|
160
|
+
};
|
|
161
|
+
const result = chain.then(run, run); // run after the predecessor settles, success or failure
|
|
162
|
+
chain = result.catch(() => []); // the caller sees a rejection on `result`; the chain stays usable
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
132
167
|
/**
|
|
133
168
|
* After a successful turn, compact the session if its context has grown past pi's threshold — a long
|
|
134
169
|
* shared (group) or 1:1 conversation otherwise overflows the model's window. pi owns the mechanism
|
|
@@ -242,7 +277,7 @@ export function createPiAgentFromHarness(options) {
|
|
|
242
277
|
// (turnContext / ToolContext.session). prompt() starts the async work synchronously here, so the
|
|
243
278
|
// store propagates to the tool calls awaited within it.
|
|
244
279
|
const opts = await toPiPromptOptions(prompt);
|
|
245
|
-
const run = turnContext.run({ session: scope.session }, () => harness.prompt(prompt.text, opts));
|
|
280
|
+
const run = turnContext.run({ session: scope.session, tools: toolActivation(harness) }, () => harness.prompt(prompt.text, opts));
|
|
246
281
|
yield* queue.drainUntil(run);
|
|
247
282
|
let terminal;
|
|
248
283
|
try {
|
package/dist/engines/pi/login.js
CHANGED
|
@@ -6,7 +6,7 @@ function anySignal(...signals) {
|
|
|
6
6
|
return present.length === 0 ? undefined : present.length === 1 ? present[0] : AbortSignal.any(present);
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Map pi-ai's `
|
|
9
|
+
* Map pi-ai's `AuthInteraction` onto the injected {@link LoginIO}. `doneSignal` fires when the flow
|
|
10
10
|
* resolves, cancelling a prompt the provider left pending (a manual-code paste racing a callback
|
|
11
11
|
* server it just won) so the one-shot CLI exits instead of hanging on stdin.
|
|
12
12
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
|
|
3
|
+
export declare function withSearchTool(tools: AgentTool[]): AgentTool[];
|
|
4
|
+
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
5
|
+
*
|
|
6
|
+
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
7
|
+
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
8
|
+
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
9
|
+
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
10
|
+
export declare function makeSearchToolsTool(): AgentTool;
|