@fastagent-sh/fastagent 0.13.0 → 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 +2 -2
- 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 +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +4 -4
- package/dist/channels/feishu/feishu-api.js +2 -2
- package/dist/channels/feishu/feishu.d.ts +11 -1
- package/dist/channels/feishu/feishu.js +143 -30
- package/dist/channels/feishu/invoke-turn.d.ts +19 -13
- package/dist/channels/feishu/invoke-turn.js +70 -19
- 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 +20 -102
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.js +2 -2
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/seen.js +12 -12
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- 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.js +8 -1329
- 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/preflight.js +4 -3
- package/dist/deploy/runner.d.ts +4 -1
- package/dist/deploy/runner.js +1 -0
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/chat.js +77 -4
- 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 -5
- package/dist/engines/pi/create.js +44 -9
- 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/pi.d.ts +2 -1
- package/dist/scaffold/add-channel.js +3 -1
- package/dist/scaffold/templates/fastagent.config.mjs +1 -0
- package/package.json +7 -4
|
@@ -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;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `search_tools` loader — the discovery surface for deferred tools (defineTool
|
|
3
|
+
* `deferred: true`). A deferred tool's schema is not in the request and the model cannot see it; this
|
|
4
|
+
* loader is how it finds and activates one. Mounted automatically (withSearchTool) only when a
|
|
5
|
+
* deferred tool exists; a workspace tool named `search_tools` wins — the author owns the concept then
|
|
6
|
+
* (same rule as the wake pair).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { log } from "../../log.js";
|
|
10
|
+
import { defineTool, isDeferredTool, stripDeferredMarker } from "./tool.js";
|
|
11
|
+
/** Mount the built-in loader iff any mounted tool is deferred and the author didn't define their own. */
|
|
12
|
+
export function withSearchTool(tools) {
|
|
13
|
+
if (!tools.some(isDeferredTool))
|
|
14
|
+
return tools;
|
|
15
|
+
const authored = tools.find((t) => t.name === "search_tools");
|
|
16
|
+
if (!authored)
|
|
17
|
+
return [...tools, makeSearchToolsTool()];
|
|
18
|
+
let fixed = authored;
|
|
19
|
+
if (isDeferredTool(fixed)) {
|
|
20
|
+
// A deferred LOADER is a contradiction — it is the only entry point to the deferred tools, so
|
|
21
|
+
// nothing could ever activate it (or, through it, them): every deferred tool would be silently
|
|
22
|
+
// unreachable. Ignore the marker and keep the loader active (fail visibly, keep the capability).
|
|
23
|
+
log.warn("[fastagent] search_tools is marked deferred — ignoring the marker: the loader must stay active, or no deferred tool could ever be activated");
|
|
24
|
+
fixed = stripDeferredMarker(fixed);
|
|
25
|
+
}
|
|
26
|
+
if (fixed.executionMode !== "sequential") {
|
|
27
|
+
// A non-sequential loader silently revives the parallel double-attribution (pi's diff around SDK
|
|
28
|
+
// tools in chat) — enforce the mode rather than hope the author read the docs; warn so they know.
|
|
29
|
+
log.warn('[fastagent] search_tools lacks executionMode: "sequential" — forcing it: parallel loader calls would misattribute activations');
|
|
30
|
+
fixed = { ...fixed, executionMode: "sequential" };
|
|
31
|
+
}
|
|
32
|
+
return fixed === authored ? tools : tools.map((t) => (t === authored ? fixed : t));
|
|
33
|
+
}
|
|
34
|
+
/** Activation cap per search: activation is additive, session-persisted, and has NO deactivate path —
|
|
35
|
+
* without a cap, one broad token ("get", "file") would permanently activate half the catalog and
|
|
36
|
+
* silently spend the entire deferral benefit for the rest of the conversation. Over the cap nothing
|
|
37
|
+
* activates; the model gets the candidates and narrows the query. */
|
|
38
|
+
const MAX_ACTIVATIONS_PER_SEARCH = 5;
|
|
39
|
+
/** Miss-path listing cap — same rationale as the activation cap: a typo query must not pour the whole
|
|
40
|
+
* catalog (the thing deferral keeps OUT of the context) back in as a tool result. */
|
|
41
|
+
const MAX_MISS_LISTING = 10;
|
|
42
|
+
/** Build the `search_tools` loader. Keyword search over the inactive tools' name+description.
|
|
43
|
+
*
|
|
44
|
+
* `executionMode: "sequential"` — pi turns any batch containing a sequential tool serial. Required for
|
|
45
|
+
* correct load-point attribution everywhere an OUTER active-set diff exists: pi wraps SDK customTools
|
|
46
|
+
* (the chat path) in a before/after diff, and two parallel loader calls would both snapshot the
|
|
47
|
+
* pre-activation set and get stamped with the same activation. Custom loader authors must set it too. */
|
|
48
|
+
export function makeSearchToolsTool() {
|
|
49
|
+
return defineTool({
|
|
50
|
+
name: "search_tools",
|
|
51
|
+
executionMode: "sequential",
|
|
52
|
+
description:
|
|
53
|
+
// First line short on purpose: the base prompt's tools list truncates at the first newline, and
|
|
54
|
+
// the discovery guidance below would otherwise flood it (and duplicate its deferred note).
|
|
55
|
+
"Discover and activate additional tools.\n" +
|
|
56
|
+
"Part of this agent's toolset is inactive until needed: search by keywords (e.g. what you are " +
|
|
57
|
+
"trying to do), and matching inactive tools are activated and become callable from that point on " +
|
|
58
|
+
"(if too many match, you get the candidates back — narrow the query, or query an exact tool " +
|
|
59
|
+
"name). ALWAYS search here before concluding a capability is missing.",
|
|
60
|
+
input: z.object({
|
|
61
|
+
query: z.string().min(1).describe("keywords describing the capability you need (e.g. 'weather forecast')"),
|
|
62
|
+
}),
|
|
63
|
+
async execute(input, ctx) {
|
|
64
|
+
if (!ctx.tools)
|
|
65
|
+
return "tool activation is unavailable outside a conversation turn.";
|
|
66
|
+
// Search the WHOLE registered catalog: the loader is the only discovery surface, and in a long
|
|
67
|
+
// conversation the model does not remember what it activated — a "No tools matched" answer for
|
|
68
|
+
// an ALREADY-ACTIVE tool would push it toward the exact wrong conclusion (capability missing).
|
|
69
|
+
// ponytail: naive keyword match (any query token as a case-insensitive substring of
|
|
70
|
+
// name+description) with a hard per-search activation cap above — the two named ceilings are
|
|
71
|
+
// relevance and irreversibility; swap in scoring/embeddings if catalogs outgrow this.
|
|
72
|
+
const tokens = input.query
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.split(/[^a-z0-9]+/)
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
// AND semantics: EVERY token must hit — "adding a word narrows" must actually hold, or the
|
|
77
|
+
// over-cap "narrow the query" instruction sends the model in circles (OR would widen with each
|
|
78
|
+
// word, and a shared prefix like "fetch" could make a whole tool family permanently over-cap).
|
|
79
|
+
const matchesQuery = (t) => {
|
|
80
|
+
const haystack = `${t.name} ${t.description}`.toLowerCase();
|
|
81
|
+
return tokens.every((token) => haystack.includes(token));
|
|
82
|
+
};
|
|
83
|
+
const describe = (t) => `${t.name} — ${t.description.split("\n")[0]}`;
|
|
84
|
+
const active = new Set(ctx.tools.active());
|
|
85
|
+
const registered = ctx.tools.registered();
|
|
86
|
+
// A query with no searchable tokens (all punctuation/symbols) must not match: `every` over an
|
|
87
|
+
// empty token list is vacuously true, and a noise query would otherwise activate the catalog.
|
|
88
|
+
if (tokens.length === 0)
|
|
89
|
+
return `"${input.query}" contains no searchable keywords — describe the capability you need.`;
|
|
90
|
+
// Exact-name shortcut: the guaranteed escape hatch from the cap — a query that IS an INACTIVE
|
|
91
|
+
// registered tool's name addresses that one tool, no keyword scoring in the way. An exact match
|
|
92
|
+
// on an ACTIVE tool falls through to keyword matching: it must not swallow the discovery of
|
|
93
|
+
// other, still-inactive keyword matches.
|
|
94
|
+
const exact = registered.find((t) => t.name.toLowerCase() === input.query.trim().toLowerCase());
|
|
95
|
+
const activeMatches = registered.filter((t) => active.has(t.name) && (t === exact || matchesQuery(t)));
|
|
96
|
+
const inactiveMatches = exact && !active.has(exact.name) ? [exact] : registered.filter((t) => !active.has(t.name) && matchesQuery(t));
|
|
97
|
+
// Same listing cap as every other branch — a wide token can match most of the ACTIVE set too
|
|
98
|
+
// (in chat that includes pi's default tools), and no answer may pour a catalog into the context.
|
|
99
|
+
const listedActive = activeMatches.slice(0, MAX_MISS_LISTING);
|
|
100
|
+
const moreActive = activeMatches.length - listedActive.length;
|
|
101
|
+
const activeNote = activeMatches.length > 0
|
|
102
|
+
? `Already active (call directly): ${listedActive.map(describe).join("; ")}${moreActive > 0 ? ` … and ${moreActive} more` : ""}.`
|
|
103
|
+
: "";
|
|
104
|
+
if (inactiveMatches.length === 0) {
|
|
105
|
+
if (activeNote)
|
|
106
|
+
return activeNote;
|
|
107
|
+
const inactive = registered.filter((t) => !active.has(t.name));
|
|
108
|
+
if (inactive.length === 0)
|
|
109
|
+
return "All tools are already active — nothing to discover.";
|
|
110
|
+
// Cap the miss listing like the activation cap — both guard the same semantic (don't pour the
|
|
111
|
+
// catalog back into the context the deferral exists to protect).
|
|
112
|
+
const listed = inactive.slice(0, MAX_MISS_LISTING);
|
|
113
|
+
const more = inactive.length - listed.length;
|
|
114
|
+
return `No tools matched "${input.query}". Inactive tools: ${listed.map(describe).join("; ")}${more > 0 ? ` … and ${more} more — search with different keywords.` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
if (inactiveMatches.length > MAX_ACTIVATIONS_PER_SEARCH) {
|
|
117
|
+
// Same listing cap as the miss path — an over-cap answer must not pour the catalog into the
|
|
118
|
+
// context either. Names alone suffice: the exact-name escape only needs a name to query.
|
|
119
|
+
const listed = inactiveMatches.slice(0, MAX_MISS_LISTING);
|
|
120
|
+
const more = inactiveMatches.length - listed.length;
|
|
121
|
+
return `${inactiveMatches.length} inactive tools matched "${input.query}" — too many to activate at once (activation is permanent for this conversation). Narrow the query (or query an exact name). Matches: ${listed
|
|
122
|
+
.map((t) => t.name)
|
|
123
|
+
.join(", ")}${more > 0 ? ` … and ${more} more` : ""}.${activeNote ? ` ${activeNote}` : ""}`;
|
|
124
|
+
}
|
|
125
|
+
const activated = await ctx.tools.activate(inactiveMatches.map((t) => t.name));
|
|
126
|
+
// Report what actually happened, not what was attempted: a parallel sibling call may have
|
|
127
|
+
// activated the same matches first, leaving nothing new here — an empty "Activated:" would lie.
|
|
128
|
+
if (activated.length === 0) {
|
|
129
|
+
return `Matched ${inactiveMatches.map((t) => t.name).join(", ")} — already active (possibly activated by a concurrent call). Call them directly.${activeNote ? ` ${activeNote}` : ""}`;
|
|
130
|
+
}
|
|
131
|
+
const raced = inactiveMatches.filter((t) => !activated.includes(t.name));
|
|
132
|
+
return `Activated: ${inactiveMatches
|
|
133
|
+
.filter((t) => activated.includes(t.name))
|
|
134
|
+
.map(describe)
|
|
135
|
+
.join("; ")}.${raced.length > 0 ? ` Already active: ${raced.map((t) => t.name).join(", ")}.` : ""}${activeNote ? ` ${activeNote}` : ""} These tools are callable now.`;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -10,8 +10,36 @@
|
|
|
10
10
|
* build time (e.g. a stateRoot) do NOT belong here — pass them via the tool's own closure.
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
/**
|
|
14
|
+
* The turn's tool-activation bridge — narrow closures over the CURRENT harness (invoke.ts builds it
|
|
15
|
+
* per turn), so a loader tool can activate deferred tools mid-turn without tool.ts importing the
|
|
16
|
+
* harness. pi records the change in the session (`active_tools_change`) and the per-invoke restore
|
|
17
|
+
* (harness.ts) carries it into later turns; defineTool's wrapper stamps the newly-activated names on
|
|
18
|
+
* the tool result (`addedToolNames`) — the load point native deferred-loading providers preserve the
|
|
19
|
+
* prompt-cache prefix with.
|
|
20
|
+
*/
|
|
21
|
+
export interface ToolActivation {
|
|
22
|
+
/** Names of the currently ACTIVE tools. */
|
|
23
|
+
active(): string[];
|
|
24
|
+
/** Every registered tool (active or not) — the discovery corpus for a loader like `search_tools`. */
|
|
25
|
+
registered(): Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** ADDITIVE activation. Unknown names are filtered out before reaching pi (whose `setActiveTools`
|
|
30
|
+
* THROWS on them); resolves the names actually newly activated (already-active names don't repeat). */
|
|
31
|
+
activate(names: string[]): Promise<string[]>;
|
|
32
|
+
}
|
|
13
33
|
export interface TurnContext {
|
|
14
34
|
/** The session id of the current turn. */
|
|
15
35
|
session: string;
|
|
36
|
+
/** Tool activation for the current turn. Two producers, one consumer surface: invoke.ts bridges the
|
|
37
|
+
* serving harness; chat.ts bridges pi's AgentSession (chat emulates deferral — same loader, same
|
|
38
|
+
* semantics). Absent only outside any turn (a bare `fastagent tool` run). */
|
|
39
|
+
tools?: ToolActivation;
|
|
16
40
|
}
|
|
17
41
|
export declare const turnContext: AsyncLocalStorage<TurnContext>;
|
|
42
|
+
/** The additive-activation contract, in ONE place for both bridges (invoke.ts over the harness,
|
|
43
|
+
* chat.ts over pi's AgentSession): dedupe → keep registered names only (pi's setters THROW on
|
|
44
|
+
* unknown) → exclude already-active → the names to actually add (empty = nothing to set). */
|
|
45
|
+
export declare function additiveActivation(registered: string[], current: string[], names: string[]): string[];
|
|
@@ -11,3 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
export const turnContext = new AsyncLocalStorage();
|
|
14
|
+
/** The additive-activation contract, in ONE place for both bridges (invoke.ts over the harness,
|
|
15
|
+
* chat.ts over pi's AgentSession): dedupe → keep registered names only (pi's setters THROW on
|
|
16
|
+
* unknown) → exclude already-active → the names to actually add (empty = nothing to set). */
|
|
17
|
+
export function additiveActivation(registered, current, names) {
|
|
18
|
+
const known = new Set(registered);
|
|
19
|
+
const active = new Set(current);
|
|
20
|
+
return [...new Set(names)].filter((name) => known.has(name) && !active.has(name));
|
|
21
|
+
}
|
|
@@ -1,22 +1,53 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
4
|
+
import { type ToolActivation } from "./tool-context.ts";
|
|
4
5
|
export interface ToolContext {
|
|
5
6
|
/** Abort signal for the current turn — honor it to cancel in-flight work on cancellation. */
|
|
6
7
|
signal?: AbortSignal;
|
|
7
8
|
/** The session id of the current turn — which conversation this tool is running in. A general tool
|
|
8
9
|
* capability: partition per-conversation data, tag logs, scope state. Undefined outside a turn (a bare
|
|
9
10
|
* `fastagent tool` run, or any call with no session). (The built-in `wake` tool is one consumer — it
|
|
10
|
-
* fires a later turn back into this same session.)
|
|
11
|
+
* fires a later turn back into this same session.) In a `fastagent chat` turn this is pi's LOCAL
|
|
12
|
+
* chat session id, not a served session — serving-coupled consumers like wake are not mounted there. */
|
|
11
13
|
session?: string;
|
|
14
|
+
/** Tool activation for the current turn (a loader tool activates {@link DefineToolOptions.deferred}
|
|
15
|
+
* tools with it — the built-in `search_tools` is one consumer). Provided by both the serving path
|
|
16
|
+
* (invoke.ts, over the harness) and chat (over pi's AgentSession); undefined only outside any turn
|
|
17
|
+
* (a bare `fastagent tool` run). */
|
|
18
|
+
tools?: ToolActivation;
|
|
12
19
|
}
|
|
13
20
|
export interface DefineToolOptions<I extends z.ZodType> {
|
|
14
21
|
/** Explicit name. Usually omitted — a `tools/<name>.ts` tool is named from its filename. */
|
|
15
22
|
name?: string;
|
|
16
23
|
description: string;
|
|
17
24
|
input: I;
|
|
25
|
+
/**
|
|
26
|
+
* Registered but NOT initially active: the tool's schema stays out of every request (and the model's
|
|
27
|
+
* sight) until a loader — the built-in `search_tools`, mounted automatically when any deferred tool
|
|
28
|
+
* exists — activates it mid-turn. For tool-heavy agents: fewer schemas per turn, and on providers
|
|
29
|
+
* with native deferred loading the activation preserves the prompt-cache prefix. The trade-off:
|
|
30
|
+
* discovery rides entirely on this description — write it for the search. Default: false.
|
|
31
|
+
*/
|
|
32
|
+
deferred?: boolean;
|
|
33
|
+
/** pi's per-tool execution mode: "sequential" makes pi run any batch containing this tool serially.
|
|
34
|
+
* REQUIRED for a tool that activates others (a loader): pi's own diff around SDK tools (chat)
|
|
35
|
+
* would attribute one activation to two parallel calls otherwise. */
|
|
36
|
+
executionMode?: "sequential" | "parallel";
|
|
18
37
|
execute: (input: z.infer<I>, ctx: ToolContext) => unknown | Promise<unknown>;
|
|
19
38
|
}
|
|
39
|
+
/** An AgentTool with fastagent's deferral marker — the type for raw tools handed to fastagent
|
|
40
|
+
* (`config.tools`, L1/L2 `tools`): plain `AgentTool` has no `deferred`, so an object literal with the
|
|
41
|
+
* marker would fail excess-property checking against upstream's type. `defineTool` produces it. */
|
|
42
|
+
export type FastagentTool = AgentTool & {
|
|
43
|
+
deferred?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** Read the {@link DefineToolOptions.deferred} marker off a mounted tool (extra property on the
|
|
46
|
+
* AgentTool object — pi ignores it). */
|
|
47
|
+
export declare function isDeferredTool(tool: AgentTool): boolean;
|
|
48
|
+
/** The same tool without the deferred marker — for a loader that must stay active (a deferred loader
|
|
49
|
+
* could never be activated and would strand every deferred tool). */
|
|
50
|
+
export declare function stripDeferredMarker(tool: AgentTool): AgentTool;
|
|
20
51
|
export declare function defineTool<I extends z.ZodType>(options: DefineToolOptions<I>): AgentTool;
|
|
21
52
|
/** A discarded same-name tool (within `tools/`, or against an existing tool). Surfaced, never silent. */
|
|
22
53
|
export interface ToolCollision {
|
package/dist/engines/pi/tool.js
CHANGED
|
@@ -14,6 +14,19 @@ import { join } from "node:path";
|
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { loadModuleDir } from "../../loader.js";
|
|
16
16
|
import { turnContext } from "./tool-context.js";
|
|
17
|
+
/** Read the {@link DefineToolOptions.deferred} marker off a mounted tool (extra property on the
|
|
18
|
+
* AgentTool object — pi ignores it). */
|
|
19
|
+
export function isDeferredTool(tool) {
|
|
20
|
+
return tool.deferred === true;
|
|
21
|
+
}
|
|
22
|
+
/** The same tool without the deferred marker — for a loader that must stay active (a deferred loader
|
|
23
|
+
* could never be activated and would strand every deferred tool). */
|
|
24
|
+
export function stripDeferredMarker(tool) {
|
|
25
|
+
if (!isDeferredTool(tool))
|
|
26
|
+
return tool;
|
|
27
|
+
const { deferred: _drop, ...active } = tool;
|
|
28
|
+
return active;
|
|
29
|
+
}
|
|
17
30
|
/** Wrap a plain return value into pi's tool-result shape; pass a full result through unchanged. */
|
|
18
31
|
function wrapResult(value) {
|
|
19
32
|
if (value && typeof value === "object" && Array.isArray(value.content)) {
|
|
@@ -28,6 +41,8 @@ export function defineTool(options) {
|
|
|
28
41
|
name: options.name ?? "",
|
|
29
42
|
description: options.description,
|
|
30
43
|
parameters,
|
|
44
|
+
...(options.deferred ? { deferred: true } : {}),
|
|
45
|
+
...(options.executionMode ? { executionMode: options.executionMode } : {}),
|
|
31
46
|
async execute(_toolCallId, rawParams, signal) {
|
|
32
47
|
const parsed = options.input.safeParse(rawParams);
|
|
33
48
|
if (!parsed.success) {
|
|
@@ -35,7 +50,33 @@ export function defineTool(options) {
|
|
|
35
50
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
36
51
|
return { content: [{ type: "text", text: `Invalid arguments: ${detail}` }], details: { error: detail } };
|
|
37
52
|
}
|
|
38
|
-
|
|
53
|
+
const store = turnContext.getStore();
|
|
54
|
+
// Stamp tools THIS execute activates on its result — the load point that lets native
|
|
55
|
+
// deferred-loading providers add the definitions at this transcript position without
|
|
56
|
+
// invalidating the cached prompt prefix. The names come from this execute's OWN activate()
|
|
57
|
+
// calls (accumulated below), NOT from an active-set before/after diff: pi runs tool calls of a
|
|
58
|
+
// batch in parallel, and a snapshot diff would stamp a sibling's activation onto the wrong tool
|
|
59
|
+
// result, drifting the load point.
|
|
60
|
+
const added = [];
|
|
61
|
+
const tools = store?.tools
|
|
62
|
+
? {
|
|
63
|
+
...store.tools,
|
|
64
|
+
activate: async (names) => {
|
|
65
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by the ternary above
|
|
66
|
+
const activated = await store.tools.activate(names);
|
|
67
|
+
added.push(...activated);
|
|
68
|
+
return activated;
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: undefined;
|
|
72
|
+
const result = wrapResult(await options.execute(parsed.data, { signal, session: store?.session, tools }));
|
|
73
|
+
if (added.length > 0) {
|
|
74
|
+
// A copy, not a mutation: wrapResult passes a full AgentToolResult through by REFERENCE, and an
|
|
75
|
+
// author may legally return a shared/frozen result object — stamping in place would corrupt it
|
|
76
|
+
// across calls (or throw on frozen), only on the rare activating path.
|
|
77
|
+
return { ...result, addedToolNames: [...new Set([...(result.addedToolNames ?? []), ...added])] };
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
39
80
|
},
|
|
40
81
|
};
|
|
41
82
|
return tool;
|
|
@@ -45,8 +45,11 @@ export declare function createPiAgentFromWorkspace(dir: string, options?: Create
|
|
|
45
45
|
sessionsDir: string;
|
|
46
46
|
/** Absolute credentials file in use (for the startup report). */
|
|
47
47
|
authPath: string;
|
|
48
|
-
/** Non-default tool names in effect: config.tools + discovered tools/.
|
|
48
|
+
/** Non-default, active-by-default tool names in effect: config.tools + discovered tools/. Each name
|
|
49
|
+
* lives in exactly one report slot — deferred names are in {@link deferredToolNames} instead. */
|
|
49
50
|
toolNames: string[];
|
|
51
|
+
/** Tools registered but not initially active (deferred) — activated via search_tools. */
|
|
52
|
+
deferredToolNames: string[];
|
|
50
53
|
toolCollisions: ToolCollision[];
|
|
51
54
|
/** `tools/` files that failed to import — skipped, reported by the caller, never fatal. */
|
|
52
55
|
toolFailures: ModuleLoadFailure[];
|
|
@@ -28,7 +28,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
|
|
|
28
28
|
// The run root is `dir` (cwd — where config lives, whose AGENTS.md is ② context); the agent's own
|
|
29
29
|
// surface (persona/skills/tools/channels) lives in `agentDir` (config.agentDir, or `dir` when flat).
|
|
30
30
|
const agentDir = resolveAgentDir(dir, config);
|
|
31
|
-
const { tools, toolNames, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, dir);
|
|
31
|
+
const { tools, toolNames, deferredToolNames, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, dir);
|
|
32
32
|
// The state root: auth/sessions/channel state all derive from it, so FASTAGENT_STATE_DIR moves the
|
|
33
33
|
// whole machine-state home in one knob (a container mounts one volume); the finer overrides below
|
|
34
34
|
// still win for their specific path.
|
|
@@ -47,6 +47,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
|
|
|
47
47
|
await ensureStateRootSelfIgnored(dir, stateRoot);
|
|
48
48
|
const { agent, definition } = await createPiAgentFromDefinition(agentDir, {
|
|
49
49
|
model: modelSpec,
|
|
50
|
+
thinkingLevel: config.thinkingLevel,
|
|
50
51
|
cwd: dir,
|
|
51
52
|
tools: mountedTools,
|
|
52
53
|
authPath,
|
|
@@ -64,6 +65,7 @@ export async function createPiAgentFromWorkspace(dir, options = {}) {
|
|
|
64
65
|
sessionsDir,
|
|
65
66
|
authPath,
|
|
66
67
|
toolNames,
|
|
68
|
+
deferredToolNames,
|
|
67
69
|
toolCollisions,
|
|
68
70
|
toolFailures,
|
|
69
71
|
};
|
package/dist/pi.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createPiAgent, createPiAgentFromDefinition, type CreatePiAgentFromDefinitionOptions, type CreatePiAgentOptions, } from "./engines/pi/create.ts";
|
|
2
|
-
export { defineTool, loadTools, type DefineToolOptions, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
|
|
2
|
+
export { defineTool, loadTools, type DefineToolOptions, type FastagentTool, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
|
|
3
|
+
export type { ToolActivation } from "./engines/pi/tool-context.ts";
|
|
3
4
|
export { z } from "zod";
|
|
4
5
|
export type { AgentTool, ExecutionEnv, Session, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
5
6
|
export { loadChannels, type ChannelCollision } from "./engines/pi/channel.ts";
|
|
@@ -72,6 +72,7 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
72
72
|
},
|
|
73
73
|
],
|
|
74
74
|
steps: [
|
|
75
|
+
"optional before publishing: add the sensitive im:message.group_msg permission (tenant-admin approval) to receive unmentioned managed-thread continuations and buffer other group discussion",
|
|
75
76
|
"PUBLISH the app version on the page the CLI opened — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
|
|
76
77
|
"edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
|
|
77
78
|
"the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
|
|
@@ -94,7 +95,8 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
94
95
|
},
|
|
95
96
|
],
|
|
96
97
|
steps: [
|
|
97
|
-
"finish the console setup: enable Bot and add the permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
|
|
98
|
+
"finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
|
|
99
|
+
"optional before publishing: add the sensitive im:message.group_msg permission (tenant-admin approval) to receive unmentioned managed-thread continuations and buffer other group discussion",
|
|
98
100
|
"run `fastagent dev --tunnel` and keep it running; if auto-registration reports a config-API 404, manually switch Subscription mode to webhook, set its printed https://…/lark Request URL, save, then create + publish a version",
|
|
99
101
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
|
|
100
102
|
],
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// you have access to (`fastagent models` lists them).
|
|
8
8
|
export default {
|
|
9
9
|
// model: "openai-codex/gpt-5.5",
|
|
10
|
+
// thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
|
|
10
11
|
http: { port: 8787 },
|
|
11
12
|
// selfSchedule: true, // mount the built-in `wake` tool: the agent schedules its own follow-up turns
|
|
12
13
|
// // ("check the deploy in 10 min"). Cron jobs need no opt-in — drop a schedules/<name>.ts.
|