@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.
Files changed (151) hide show
  1. package/README.md +6 -4
  2. package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
  3. package/dist/channels/feishu/bootstrap-token.js +94 -0
  4. package/dist/channels/feishu/card.d.ts +32 -0
  5. package/dist/channels/feishu/card.js +66 -0
  6. package/dist/channels/feishu/cloud.d.ts +17 -0
  7. package/dist/channels/feishu/cloud.js +19 -0
  8. package/dist/channels/feishu/context-buffer.d.ts +46 -0
  9. package/dist/channels/feishu/context-buffer.js +133 -0
  10. package/dist/channels/feishu/crypto.d.ts +15 -0
  11. package/dist/channels/feishu/crypto.js +43 -0
  12. package/dist/channels/feishu/feishu-api.d.ts +108 -0
  13. package/dist/channels/feishu/feishu-api.js +325 -0
  14. package/dist/channels/feishu/feishu.d.ts +46 -0
  15. package/dist/channels/feishu/feishu.js +472 -0
  16. package/dist/channels/feishu/invoke-turn.d.ts +65 -0
  17. package/dist/channels/feishu/invoke-turn.js +157 -0
  18. package/dist/channels/feishu/model.d.ts +97 -0
  19. package/dist/channels/feishu/model.js +9 -0
  20. package/dist/channels/feishu/normalize.d.ts +22 -0
  21. package/dist/channels/feishu/normalize.js +132 -0
  22. package/dist/channels/feishu/owned-threads.d.ts +11 -0
  23. package/dist/channels/feishu/owned-threads.js +47 -0
  24. package/dist/channels/feishu/parse.d.ts +43 -0
  25. package/dist/channels/feishu/parse.js +65 -0
  26. package/dist/channels/feishu/preview.d.ts +36 -0
  27. package/dist/channels/feishu/preview.js +387 -0
  28. package/dist/channels/feishu/register-app.d.ts +70 -0
  29. package/dist/channels/feishu/register-app.js +141 -0
  30. package/dist/channels/feishu/register-webhook.d.ts +22 -0
  31. package/dist/channels/feishu/register-webhook.js +106 -0
  32. package/dist/channels/feishu/scaffold/channel.ts +41 -0
  33. package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
  34. package/dist/channels/feishu/seen.d.ts +5 -0
  35. package/dist/channels/feishu/seen.js +47 -0
  36. package/dist/channels/feishu/text.d.ts +13 -0
  37. package/dist/channels/feishu/text.js +63 -0
  38. package/dist/channels/lark/lark.d.ts +15 -0
  39. package/dist/channels/lark/lark.js +10 -0
  40. package/dist/channels/lark/onboard.d.ts +39 -0
  41. package/dist/channels/lark/onboard.js +58 -0
  42. package/dist/channels/lark/scaffold/channel.ts +39 -0
  43. package/dist/channels/lark/scaffold/lark-send.ts +87 -0
  44. package/dist/channels/registration.d.ts +15 -0
  45. package/dist/channels/registration.js +1 -0
  46. package/dist/channels/{telegram/state.js → state.js} +6 -4
  47. package/dist/channels/telegram/context-buffer.js +1 -1
  48. package/dist/channels/telegram/register-webhook.d.ts +4 -1
  49. package/dist/channels/telegram/register-webhook.js +17 -26
  50. package/dist/channels/telegram/telegram.js +2 -2
  51. package/dist/channels/telegram/turn-store.d.ts +8 -21
  52. package/dist/channels/telegram/turn-store.js +11 -130
  53. package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
  54. package/dist/channels/turn-store.d.ts +42 -0
  55. package/dist/channels/turn-store.js +139 -0
  56. package/dist/channels/wait-health.d.ts +6 -0
  57. package/dist/channels/wait-health.js +27 -0
  58. package/dist/cli/commands/add.d.ts +9 -0
  59. package/dist/cli/commands/add.js +142 -0
  60. package/dist/cli/commands/chat.d.ts +3 -0
  61. package/dist/cli/commands/chat.js +16 -0
  62. package/dist/cli/commands/deploy.d.ts +13 -0
  63. package/dist/cli/commands/deploy.js +338 -0
  64. package/dist/cli/commands/dev.d.ts +11 -0
  65. package/dist/cli/commands/dev.js +76 -0
  66. package/dist/cli/commands/fire.d.ts +7 -0
  67. package/dist/cli/commands/fire.js +45 -0
  68. package/dist/cli/commands/info.d.ts +7 -0
  69. package/dist/cli/commands/info.js +108 -0
  70. package/dist/cli/commands/init.d.ts +8 -0
  71. package/dist/cli/commands/init.js +81 -0
  72. package/dist/cli/commands/invoke.d.ts +7 -0
  73. package/dist/cli/commands/invoke.js +29 -0
  74. package/dist/cli/commands/login.d.ts +6 -0
  75. package/dist/cli/commands/login.js +63 -0
  76. package/dist/cli/commands/models.d.ts +1 -0
  77. package/dist/cli/commands/models.js +15 -0
  78. package/dist/cli/commands/schedule.d.ts +12 -0
  79. package/dist/cli/commands/schedule.js +89 -0
  80. package/dist/cli/commands/start.d.ts +10 -0
  81. package/dist/cli/commands/start.js +90 -0
  82. package/dist/cli/commands/tool.d.ts +1 -0
  83. package/dist/cli/commands/tool.js +37 -0
  84. package/dist/cli/fail.d.ts +19 -0
  85. package/dist/cli/fail.js +32 -0
  86. package/dist/cli/kernel.d.ts +89 -0
  87. package/dist/cli/kernel.js +190 -0
  88. package/dist/cli/program.d.ts +11 -0
  89. package/dist/cli/program.js +421 -0
  90. package/dist/cli/serve.d.ts +28 -0
  91. package/dist/cli/serve.js +90 -0
  92. package/dist/cli/shared.d.ts +24 -0
  93. package/dist/cli/shared.js +116 -0
  94. package/dist/cli-add-feishu.d.ts +8 -0
  95. package/dist/cli-add-feishu.js +223 -0
  96. package/dist/cli.js +8 -1284
  97. package/dist/deploy/container.js +10 -6
  98. package/dist/deploy/docker/plan.d.ts +45 -0
  99. package/dist/deploy/docker/plan.js +139 -0
  100. package/dist/deploy/docker/run.d.ts +40 -0
  101. package/dist/deploy/docker/run.js +126 -0
  102. package/dist/deploy/fly/plan.d.ts +1 -1
  103. package/dist/deploy/fly/plan.js +15 -4
  104. package/dist/deploy/fly/run.d.ts +7 -4
  105. package/dist/deploy/fly/run.js +26 -5
  106. package/dist/deploy/preflight.js +4 -3
  107. package/dist/deploy/railway/plan.d.ts +1 -1
  108. package/dist/deploy/railway/plan.js +17 -5
  109. package/dist/deploy/railway/run.d.ts +6 -3
  110. package/dist/deploy/railway/run.js +26 -4
  111. package/dist/deploy/registration-gate.d.ts +20 -0
  112. package/dist/deploy/registration-gate.js +20 -0
  113. package/dist/deploy/runner.d.ts +4 -1
  114. package/dist/deploy/runner.js +1 -0
  115. package/dist/deploy/secrets.d.ts +10 -9
  116. package/dist/deploy/secrets.js +15 -14
  117. package/dist/dev-supervisor.js +2 -1
  118. package/dist/engines/pi/auth.js +160 -46
  119. package/dist/engines/pi/chat.js +80 -7
  120. package/dist/engines/pi/config.d.ts +12 -3
  121. package/dist/engines/pi/config.js +16 -1
  122. package/dist/engines/pi/create.d.ts +14 -6
  123. package/dist/engines/pi/create.js +52 -16
  124. package/dist/engines/pi/harness.d.ts +16 -1
  125. package/dist/engines/pi/harness.js +77 -1
  126. package/dist/engines/pi/invoke.d.ts +1 -1
  127. package/dist/engines/pi/invoke.js +37 -2
  128. package/dist/engines/pi/login.js +1 -1
  129. package/dist/engines/pi/search-tools.d.ts +10 -0
  130. package/dist/engines/pi/search-tools.js +138 -0
  131. package/dist/engines/pi/tool-context.d.ts +28 -0
  132. package/dist/engines/pi/tool-context.js +8 -0
  133. package/dist/engines/pi/tool.d.ts +32 -1
  134. package/dist/engines/pi/tool.js +42 -1
  135. package/dist/engines/pi/workspace.d.ts +4 -1
  136. package/dist/engines/pi/workspace.js +3 -1
  137. package/dist/feishu.d.ts +2 -0
  138. package/dist/feishu.js +2 -0
  139. package/dist/lark.d.ts +3 -0
  140. package/dist/lark.js +3 -0
  141. package/dist/open-url.d.ts +2 -0
  142. package/dist/open-url.js +6 -0
  143. package/dist/pi.d.ts +2 -1
  144. package/dist/scaffold/add-channel.d.ts +9 -5
  145. package/dist/scaffold/add-channel.js +75 -7
  146. package/dist/scaffold/templates/fastagent.config.mjs +1 -0
  147. package/dist/tunnel.d.ts +9 -6
  148. package/dist/tunnel.js +48 -31
  149. package/package.json +18 -4
  150. /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
  151. /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
@@ -1,9 +1,17 @@
1
- import type { AgentTool } from "@earendil-works/pi-agent-core";
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
- tools?: AgentTool[];
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
- tools?: AgentTool[];
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
- tools?: AgentTool[];
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 { tools, collisions } = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
48
- const toolCollisions = [...discovered.collisions, ...collisions];
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.map((t) => t.name).filter((n) => !defaultNames.has(n));
51
- return { tools, toolNames, toolCollisions, toolFailures: discovered.failures };
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 (date/cwd)
77
+ // + ③ skills listing + ④ env context (cwd)
58
78
  //
59
- // AGENTS.md ≠ system prompt. Pure functions: segment ④ inputs (date/cwd) are caller-provided, so the
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 tools = options.tools ?? [];
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: options.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
- const tools = options.tools ?? piDefaultTools(env.cwd);
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), `date` is the turn's date, and the
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
- return new AgentHarness({
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 { PiHarnessFactory } from "./harness.ts";
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 { turnContext } from "./tool-context.js";
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 {
@@ -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 `AuthLoginCallbacks` onto the injected {@link LoginIO}. `doneSignal` fires when the flow
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;