@fastagent-sh/fastagent 0.19.0 → 0.21.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 +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -18,7 +18,7 @@ import { log } from "../../log.js";
|
|
|
18
18
|
import { isDeferredTool } from "./tool.js";
|
|
19
19
|
import { activePath, resolveSessionSettings } from "./session-settings.js";
|
|
20
20
|
import { DEFAULT_THINKING_LEVEL } from "./models.js";
|
|
21
|
-
import {
|
|
21
|
+
import { agentSessionManager, sessionToolActivation, turnContext } from "./tool-context.js";
|
|
22
22
|
/**
|
|
23
23
|
* The session custom-entry type recording ONE activation delta: `{ names }` — exactly the deferred
|
|
24
24
|
* tools a loader activated in that call.
|
|
@@ -47,37 +47,6 @@ function recordedActivations(session) {
|
|
|
47
47
|
/** Warned once per session+missing set: a fresh session is built per invoke and channel sessions run
|
|
48
48
|
* for weeks, so an un-deduped warn would repeat every turn and dilute its own signal. */
|
|
49
49
|
const warnedDroppedActivations = new Set();
|
|
50
|
-
/**
|
|
51
|
-
* The turn's {@link ToolActivation} over a live session — the same bridge chat uses, so one
|
|
52
|
-
* built-in `search_tools` serves both.
|
|
53
|
-
*
|
|
54
|
-
* Activations are PERSISTED as deltas, so a tool discovered in one turn stays callable in the next.
|
|
55
|
-
* pi's own chat session does not do this (it has no place to put the record); a served session does,
|
|
56
|
-
* because the alternative is an agent that re-discovers the same capability every single turn.
|
|
57
|
-
*/
|
|
58
|
-
function sessionToolActivation(session) {
|
|
59
|
-
// Serialize activations: the read-modify-write below is only race-free while nothing awaits
|
|
60
|
-
// between read and write, and parallel tool calls in one batch would otherwise double-stamp.
|
|
61
|
-
let chain = Promise.resolve([]);
|
|
62
|
-
return {
|
|
63
|
-
active: () => session.getActiveToolNames(),
|
|
64
|
-
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
65
|
-
activate(names) {
|
|
66
|
-
const run = async () => {
|
|
67
|
-
const current = session.getActiveToolNames();
|
|
68
|
-
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
69
|
-
if (added.length > 0) {
|
|
70
|
-
session.setActiveToolsByName([...current, ...added]);
|
|
71
|
-
session.sessionManager.appendCustomEntry(TOOL_ACTIVATION_ENTRY, { names: added });
|
|
72
|
-
}
|
|
73
|
-
return added;
|
|
74
|
-
};
|
|
75
|
-
const result = chain.then(run, run);
|
|
76
|
-
chain = result.catch(() => []);
|
|
77
|
-
return result;
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
50
|
/**
|
|
82
51
|
* fastagent's tools as pi tool definitions, bound to ONE session.
|
|
83
52
|
*
|
|
@@ -86,26 +55,101 @@ function sessionToolActivation(session) {
|
|
|
86
55
|
* throws rather than executing outside the turn: a broken lifecycle must not look like a normal
|
|
87
56
|
* out-of-turn call.
|
|
88
57
|
*/
|
|
89
|
-
function toolDefinitions(tools,
|
|
58
|
+
function toolDefinitions(tools, bound, sessionId) {
|
|
90
59
|
return tools.map((tool) => ({
|
|
91
|
-
|
|
92
|
-
label: tool.name,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
60
|
+
...tool,
|
|
61
|
+
label: tool.label || tool.name,
|
|
62
|
+
execute: (id, params, signal, onUpdate, ctx) => {
|
|
63
|
+
const binding = bound.current;
|
|
64
|
+
if (!binding)
|
|
65
|
+
throw new Error("tool executed before its turn context was bound (lifecycle invariant broken)");
|
|
66
|
+
const { session, context } = binding;
|
|
67
|
+
// Expose the caller's id rather than Pi's filename encoding.
|
|
68
|
+
const sessionManager = Object.create(ctx.sessionManager, {
|
|
69
|
+
getSessionId: { value: () => sessionId },
|
|
70
|
+
getHeader: {
|
|
71
|
+
value: () => {
|
|
72
|
+
const header = ctx.sessionManager.getHeader();
|
|
73
|
+
return header ? { ...header, id: sessionId } : header;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// Pi's thinking getter uses a shared runtime; a restored cwd may be a symlink spelling.
|
|
78
|
+
const scoped = Object.create(ctx, {
|
|
79
|
+
sessionManager: { value: sessionManager },
|
|
80
|
+
cwd: { value: context.cwd },
|
|
81
|
+
thinkingLevel: { get: () => session.thinkingLevel },
|
|
82
|
+
});
|
|
83
|
+
return turnContext.run(context, () => tool.execute(id, params, signal, onUpdate, scoped));
|
|
106
84
|
},
|
|
107
85
|
}));
|
|
108
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Bind ONE pi session to a record: the definition's tools as pi definitions over one turn context,
|
|
89
|
+
* pi's own tool copies kept off, and deferral applied. The per-invoke factory and the resident chat
|
|
90
|
+
* runtime both bind here — they differ in what they hand in (a shared vs a per-session `services`,
|
|
91
|
+
* record-resolved vs configured settings) and in whether a discovered activation has a record to
|
|
92
|
+
* land in, and in nothing else. Two copies of this drifted once (the tool adapter and the deferral
|
|
93
|
+
* narrowing each existed twice, identical but for those parameters).
|
|
94
|
+
*/
|
|
95
|
+
export async function bindPiSession(options) {
|
|
96
|
+
const { services, sessionManager, model, thinkingLevel, tools, cwd, recordActivations } = options;
|
|
97
|
+
const excludedToolNames = options.excludedToolNames ?? [];
|
|
98
|
+
const deferred = tools.filter(isDeferredTool).map((t) => t.name);
|
|
99
|
+
const bound = {};
|
|
100
|
+
const sessionId = options.sessionId ?? sessionManager.getSessionId();
|
|
101
|
+
const result = await createAgentSessionFromServices({
|
|
102
|
+
services,
|
|
103
|
+
sessionManager,
|
|
104
|
+
...(options.sessionStartEvent ? { sessionStartEvent: options.sessionStartEvent } : {}),
|
|
105
|
+
model,
|
|
106
|
+
thinkingLevel,
|
|
107
|
+
// pi would otherwise mount its built-ins on top of fastagent's copies, offering duplicate names.
|
|
108
|
+
// Lower-level callers with an explicit list also rely on omitted built-ins staying omitted. And
|
|
109
|
+
// NO `tools` allowlist: it would freeze the set at bind time, while pi lets an extension register
|
|
110
|
+
// from `session_start` — `noTools: "builtin"` gives the guarantee the allowlist was there for.
|
|
111
|
+
noTools: "builtin",
|
|
112
|
+
...(excludedToolNames.length > 0 ? { excludeTools: [...excludedToolNames] } : {}),
|
|
113
|
+
customTools: toolDefinitions(tools, bound, sessionId),
|
|
114
|
+
});
|
|
115
|
+
const { session } = result;
|
|
116
|
+
// One context for the whole session: it describes the SESSION, not the call. The activation
|
|
117
|
+
// bridge above all — a tool call has to see what the previous one activated.
|
|
118
|
+
bound.current = {
|
|
119
|
+
session,
|
|
120
|
+
context: {
|
|
121
|
+
cwd,
|
|
122
|
+
sessionManager: agentSessionManager(session, sessionId),
|
|
123
|
+
// Persist each discovery so a served session can restore it on its next turn.
|
|
124
|
+
tools: sessionToolActivation(session, recordActivations
|
|
125
|
+
? (added) => session.sessionManager.appendCustomEntry(TOOL_ACTIVATION_ENTRY, { names: added })
|
|
126
|
+
: undefined),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
// Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the
|
|
130
|
+
// deferred names (robust to pi mounting tools of its own, unlike an exact-set replacement), then
|
|
131
|
+
// add back what THIS session has already discovered.
|
|
132
|
+
if (deferred.length > 0) {
|
|
133
|
+
const active = session.getActiveToolNames();
|
|
134
|
+
const mounted = new Set(session.getAllTools().map((tool) => tool.name));
|
|
135
|
+
const recorded = recordActivations ? recordedActivations(session) : [];
|
|
136
|
+
// A recorded name that is no longer mounted is dropped rather than replayed: pi's setter
|
|
137
|
+
// THROWS on an unknown name, so replaying one would brick every future turn of this session.
|
|
138
|
+
const restored = recorded.filter((name) => mounted.has(name));
|
|
139
|
+
const dropped = recorded.filter((name) => !mounted.has(name));
|
|
140
|
+
if (dropped.length > 0) {
|
|
141
|
+
const key = `${sessionId}\u0000${[...new Set(dropped)].sort().join(",")}`;
|
|
142
|
+
const emit = warnedDroppedActivations.has(key) ? log.debug : log.warn;
|
|
143
|
+
warnedDroppedActivations.add(key);
|
|
144
|
+
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${[...new Set(dropped)].join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
const next = [...new Set([...active.filter((name) => !deferred.includes(name)), ...restored])];
|
|
147
|
+
if (next.length !== active.length || next.some((name) => !active.includes(name))) {
|
|
148
|
+
session.setActiveToolsByName(next);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
109
153
|
/**
|
|
110
154
|
* Announce extensions pi failed to load. pi collects them into `LoadExtensionsResult.errors` and
|
|
111
155
|
* carries on with the rest — sound for a TUI that shows them, silent for a server that never looks.
|
|
@@ -148,7 +192,7 @@ export function definitionResourceLoaderOptions(source) {
|
|
|
148
192
|
// and substitutes its own coding-assistant identity, which an L1 agent
|
|
149
193
|
// (`createPiAgent({ model, tools })`) never asked for. pi appends its own working-directory line
|
|
150
194
|
// either way — that is engine behaviour this binding does not fight.
|
|
151
|
-
systemPromptOverride: () => source.systemPrompt()
|
|
195
|
+
systemPromptOverride: () => source.systemPrompt() || " ",
|
|
152
196
|
appendSystemPromptOverride: () => [],
|
|
153
197
|
skillsOverride: (base) => ({
|
|
154
198
|
skills: toPiSkills(source.skills()),
|
|
@@ -158,7 +202,7 @@ export function definitionResourceLoaderOptions(source) {
|
|
|
158
202
|
}
|
|
159
203
|
/** Open-or-create the record, then bind a fresh session to it. One call per invoke. */
|
|
160
204
|
export function piAgentSessionFactory(options) {
|
|
161
|
-
const { sessions, thinkingLevel, cwd
|
|
205
|
+
const { sessions, thinkingLevel, cwd } = options;
|
|
162
206
|
const extensionPaths = options.extensionPaths ?? [];
|
|
163
207
|
const excludedToolNames = options.excludedToolNames ?? [];
|
|
164
208
|
if (extensionPaths.length > 0) {
|
|
@@ -167,10 +211,8 @@ export function piAgentSessionFactory(options) {
|
|
|
167
211
|
"runs concurrent turns for different conversations. See docs/configuration.md#extensions.");
|
|
168
212
|
}
|
|
169
213
|
const tools = options.tools ?? [];
|
|
170
|
-
const deferred = tools.filter(isDeferredTool).map((t) => t.name);
|
|
171
214
|
// What the shared ResourceLoader serves, refreshed per turn before the session is built.
|
|
172
|
-
let
|
|
173
|
-
let skills = options.skills ?? [];
|
|
215
|
+
let definition;
|
|
174
216
|
let services;
|
|
175
217
|
let engine;
|
|
176
218
|
const buildServices = async (modelRuntime) => createAgentSessionServices({
|
|
@@ -182,23 +224,16 @@ export function piAgentSessionFactory(options) {
|
|
|
182
224
|
// CURRENT prompt/skills — serving refreshes both per turn, so a snapshot taken here would
|
|
183
225
|
// serve a stale definition after the first edit.
|
|
184
226
|
resourceLoaderOptions: definitionResourceLoaderOptions({
|
|
185
|
-
systemPrompt: () =>
|
|
186
|
-
skills: () => skills,
|
|
227
|
+
systemPrompt: () => definition.systemPrompt,
|
|
228
|
+
skills: () => definition.skills,
|
|
187
229
|
}),
|
|
188
230
|
});
|
|
189
231
|
return async (sessionId, inherit) => {
|
|
190
|
-
const
|
|
191
|
-
const nextPrompt = fresh
|
|
192
|
-
? fresh.systemPrompt
|
|
193
|
-
: typeof options.systemPrompt === "function"
|
|
194
|
-
? options.systemPrompt()
|
|
195
|
-
: prompt;
|
|
196
|
-
const nextSkills = fresh ? (fresh.skills ?? []) : skills;
|
|
232
|
+
const next = await options.readDefinition();
|
|
197
233
|
engine ??= options.engine();
|
|
198
234
|
const { modelRuntime, model } = await engine;
|
|
199
235
|
if (services === undefined) {
|
|
200
|
-
|
|
201
|
-
skills = nextSkills;
|
|
236
|
+
definition = next;
|
|
202
237
|
services = buildServices(modelRuntime); // assigned before any await: concurrent turns share it
|
|
203
238
|
}
|
|
204
239
|
else {
|
|
@@ -223,10 +258,10 @@ export function piAgentSessionFactory(options) {
|
|
|
223
258
|
// for both. Pinning a snapshot per turn would cost either a loader per turn or a queue in
|
|
224
259
|
// front of every bind, to buy a guarantee nothing asks for.
|
|
225
260
|
const loader = (await services).resourceLoader;
|
|
226
|
-
const definitionChanged = loader.getSystemPrompt() !==
|
|
261
|
+
const definitionChanged = loader.getSystemPrompt() !== (next.systemPrompt || " ") ||
|
|
262
|
+
skillSet(loader.getSkills().skills) !== skillSet(next.skills);
|
|
227
263
|
if (definitionChanged) {
|
|
228
|
-
|
|
229
|
-
skills = nextSkills;
|
|
264
|
+
definition = next;
|
|
230
265
|
await loader.reload();
|
|
231
266
|
}
|
|
232
267
|
}
|
|
@@ -242,55 +277,25 @@ export function piAgentSessionFactory(options) {
|
|
|
242
277
|
model,
|
|
243
278
|
thinkingLevel: thinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
244
279
|
});
|
|
245
|
-
const
|
|
246
|
-
const { session } = await createAgentSessionFromServices({
|
|
280
|
+
const { session } = await bindPiSession({
|
|
247
281
|
services: await services,
|
|
248
282
|
sessionManager,
|
|
249
283
|
model: settings.model,
|
|
250
284
|
thinkingLevel: settings.thinkingLevel,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
285
|
+
tools,
|
|
286
|
+
cwd,
|
|
287
|
+
excludedToolNames,
|
|
288
|
+
sessionId,
|
|
289
|
+
recordActivations: true,
|
|
256
290
|
});
|
|
257
|
-
bound.session = session;
|
|
258
|
-
// An extension handler that throws is otherwise dropped: pi fans errors out to registered
|
|
259
|
-
// listeners and has none by default, which on a server means a broken extension looks like an
|
|
260
|
-
// extension that simply did nothing.
|
|
261
|
-
//
|
|
262
|
-
// Deferral, then restoration: pi starts every mounted tool active, so narrow by SUBTRACTING the
|
|
263
|
-
// deferred names (robust to pi mounting tools of its own, unlike an exact-set replacement), then
|
|
264
|
-
// add back what THIS session has already discovered.
|
|
265
|
-
if (deferred.length > 0) {
|
|
266
|
-
const active = session.getActiveToolNames();
|
|
267
|
-
const mounted = new Set(session.getAllTools().map((tool) => tool.name));
|
|
268
|
-
const recorded = recordedActivations(session);
|
|
269
|
-
// A recorded name that is no longer mounted is dropped rather than replayed: pi's setter
|
|
270
|
-
// THROWS on an unknown name, so replaying one would brick every future turn of this session.
|
|
271
|
-
const restored = recorded.filter((name) => mounted.has(name));
|
|
272
|
-
const dropped = recorded.filter((name) => !mounted.has(name));
|
|
273
|
-
if (dropped.length > 0) {
|
|
274
|
-
const key = `${sessionId}\u0000${[...new Set(dropped)].sort().join(",")}`;
|
|
275
|
-
const emit = warnedDroppedActivations.has(key) ? log.debug : log.warn;
|
|
276
|
-
warnedDroppedActivations.add(key);
|
|
277
|
-
emit(`[fastagent] session ${sessionId}: dropping recorded activation(s) no longer mounted: ${[...new Set(dropped)].join(", ")}`);
|
|
278
|
-
}
|
|
279
|
-
const next = [...new Set([...active.filter((name) => !deferred.includes(name)), ...restored])];
|
|
280
|
-
if (next.length !== active.length || next.some((name) => !active.includes(name))) {
|
|
281
|
-
session.setActiveToolsByName(next);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
291
|
return session;
|
|
285
292
|
};
|
|
286
293
|
}
|
|
287
294
|
/** What a reload has to notice: the declared set, not the files behind it. */
|
|
288
295
|
function skillSet(skills) {
|
|
289
|
-
return skills
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function loadedSkillSet(skills) {
|
|
293
|
-
return skills.map((s) => `${s.name}\u0000${s.filePath ?? ""}\u0000${s.description}`).join("\u0001");
|
|
296
|
+
return skills
|
|
297
|
+
.map((s) => `${s.name}\u0000${s.filePath ?? ""}\u0000${s.description}\u0000${s.disableModelInvocation ?? false}`)
|
|
298
|
+
.join("\u0001");
|
|
294
299
|
}
|
|
295
300
|
/** fastagent's Skill (content inline) as pi's (read from filePath at invocation time). */
|
|
296
301
|
function toPiSkills(skills) {
|
package/dist/engines/pi/auth.js
CHANGED
|
@@ -20,16 +20,20 @@
|
|
|
20
20
|
* Locking is vendored here on `proper-lockfile`, with the same parameters pi's file backend used
|
|
21
21
|
* before pi 0.80.8 stopped exporting it (upstream's stated migration path for SDK consumers is a
|
|
22
22
|
* custom pi-ai `CredentialStore`, which this file is). The lock guards the WRITE path only. `read`
|
|
23
|
-
* is pi-ai's per-request hot path, so it stays UNLOCKED
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* is pi-ai's per-request hot path, so it stays UNLOCKED — safe because every write that carries a
|
|
24
|
+
* CREDENTIAL publishes by rename ({@link writeFileAtomic}): a reader sees one whole version of the
|
|
25
|
+
* file or another, never a partial one, so a rotation has no torn-read window for an unlocked read
|
|
26
|
+
* to absorb. The one in-place write left is the `{}` bootstrap below, whose window an unlocked read
|
|
27
|
+
* observes as an empty file — reported as corrupt, which for a zero-byte auth.json is the right
|
|
28
|
+
* answer either way. The write path refuses to overwrite a corrupt file (never clobbering other
|
|
29
|
+
* providers' credentials).
|
|
26
30
|
*/
|
|
27
|
-
import { chmodSync, existsSync,
|
|
31
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
32
|
import { homedir } from "node:os";
|
|
29
33
|
import { dirname, join } from "node:path";
|
|
30
|
-
import { GLOBAL_HOME_DIR, SECRETS_DIRNAME } from "../../paths.js";
|
|
34
|
+
import { GLOBAL_HOME_DIR, SECRETS_DIRNAME, SECRET_FILE_MODE, ensureSecretsDir } from "../../paths.js";
|
|
35
|
+
import { writeFileAtomic } from "../../atomic-write.js";
|
|
31
36
|
import { log } from "../../log.js";
|
|
32
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
33
37
|
import lockfile from "proper-lockfile";
|
|
34
38
|
/**
|
|
35
39
|
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
|
|
@@ -46,7 +50,7 @@ function pick(creds, providerId) {
|
|
|
46
50
|
const cred = creds[providerId];
|
|
47
51
|
return cred && (cred.type === "oauth" || cred.type === "api_key") ? cred : undefined;
|
|
48
52
|
}
|
|
49
|
-
const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf8", mode:
|
|
53
|
+
const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf8", mode: SECRET_FILE_MODE };
|
|
50
54
|
/**
|
|
51
55
|
* Serialized cross-process read-modify-write of the credentials file: exponential-backoff retries,
|
|
52
56
|
* 30s staleness, and compromise detection (the parameters pi's `FileAuthStorageBackend` used).
|
|
@@ -56,13 +60,15 @@ const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf8", mode: 0o600 };
|
|
|
56
60
|
* failed unlock after a successful operation rejects instead of leaving a stale lock silently.
|
|
57
61
|
*/
|
|
58
62
|
async function withLockedAuthFile(authPath, fn) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
// 0700 unconditionally, including on a directory an operator named with `--auth-path`: pointing a
|
|
64
|
+
// credential file somewhere is asking for that somewhere to hold a credential. Where the process
|
|
65
|
+
// cannot chmod (a mount it does not own), this raises — the write fails visibly instead of
|
|
66
|
+
// quietly leaving the directory readable, which is the trade this repo takes everywhere else.
|
|
67
|
+
await ensureSecretsDir(dirname(authPath));
|
|
62
68
|
if (!existsSync(authPath)) {
|
|
63
69
|
try {
|
|
64
70
|
writeFileSync(authPath, "{}", { ...AUTH_FILE_WRITE_OPTIONS, flag: "wx" });
|
|
65
|
-
chmodSync(authPath,
|
|
71
|
+
chmodSync(authPath, SECRET_FILE_MODE);
|
|
66
72
|
}
|
|
67
73
|
catch (error) {
|
|
68
74
|
// EEXIST: another process created the file between the existence check and this exclusive
|
|
@@ -89,10 +95,12 @@ async function withLockedAuthFile(authPath, fn) {
|
|
|
89
95
|
const current = existsSync(authPath) ? readFileSync(authPath, "utf8") : undefined;
|
|
90
96
|
const out = await fn(current);
|
|
91
97
|
throwIfCompromised();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
// Rename, not an in-place rewrite: it is what lets `read` stay unlocked, and it is the only
|
|
99
|
+
// spelling that applies the mode before the content is reachable — `writeFileSync`'s `mode` is
|
|
100
|
+
// a no-op on an existing file, so a chmod after it leaves the new credential briefly readable
|
|
101
|
+
// at whatever mode the old file carried.
|
|
102
|
+
if (out.next !== undefined)
|
|
103
|
+
writeFileAtomic(authPath, out.next, SECRET_FILE_MODE);
|
|
96
104
|
throwIfCompromised();
|
|
97
105
|
result = out.result;
|
|
98
106
|
}
|
|
@@ -139,34 +147,26 @@ function decodeCreds(raw) {
|
|
|
139
147
|
return parsed;
|
|
140
148
|
}
|
|
141
149
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
150
|
+
* UNLOCKED read of the whole credentials file, shared by `read` and `list`. ONE read: the write
|
|
151
|
+
* publishes by rename, so this observes one whole version or another and has nothing to absorb by
|
|
152
|
+
* re-reading. A missing file reads as undefined silently (normal not-configured); anything that
|
|
153
|
+
* does not decode is a real corruption — a hand-edit — and says so immediately, rather than after
|
|
154
|
+
* retrying a race that cannot happen.
|
|
147
155
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
if (raw !== "") {
|
|
161
|
-
const creds = decodeCreds(raw);
|
|
162
|
-
if (creds !== undefined)
|
|
163
|
-
return creds;
|
|
164
|
-
// A partial read mid-write parses as garbage; fall through and retry. A structurally invalid
|
|
165
|
-
// root lands here too and is reported as corrupt below.
|
|
166
|
-
}
|
|
167
|
-
if (attempt < 2)
|
|
168
|
-
await sleep(2);
|
|
156
|
+
function readCreds(authPath, warn) {
|
|
157
|
+
let raw;
|
|
158
|
+
try {
|
|
159
|
+
raw = readFileSync(authPath, "utf8");
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error.code === "ENOENT")
|
|
163
|
+
return undefined; // missing/deleted
|
|
164
|
+
warn(`[fastagent] cannot read ${authPath}: ${error.message}`);
|
|
165
|
+
return undefined;
|
|
169
166
|
}
|
|
167
|
+
const creds = raw === "" ? undefined : decodeCreds(raw);
|
|
168
|
+
if (creds !== undefined)
|
|
169
|
+
return creds;
|
|
170
170
|
warn(`[fastagent] corrupt auth file ${authPath}: fix or remove it`);
|
|
171
171
|
return undefined;
|
|
172
172
|
}
|
|
@@ -190,13 +190,13 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
|
|
|
190
190
|
const warn = options.warn ?? ((message) => log.warn(message));
|
|
191
191
|
return {
|
|
192
192
|
async read(providerId) {
|
|
193
|
-
const creds =
|
|
193
|
+
const creds = readCreds(authPath, warn);
|
|
194
194
|
return creds ? pick(creds, providerId) : undefined;
|
|
195
195
|
},
|
|
196
196
|
async list() {
|
|
197
197
|
// Metadata only, never secrets (the pi-ai `list` contract). Foreign/old entries are filtered
|
|
198
198
|
// with the same validation as `read`, so both surfaces agree on what "configured" means.
|
|
199
|
-
const creds =
|
|
199
|
+
const creds = readCreds(authPath, warn);
|
|
200
200
|
if (!creds)
|
|
201
201
|
return [];
|
|
202
202
|
const infos = [];
|
|
@@ -24,12 +24,12 @@ export interface FastagentConfig {
|
|
|
24
24
|
selfSchedule?: boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Serve the session control plane over HTTP (`/control/*`: state/entries/events + dispatch —
|
|
27
|
-
* steer/abort/compact
|
|
27
|
+
* steer/abort/compact + session properties and lifecycle) for remote consumers: a Web panel, a desktop app, `fastagent
|
|
28
28
|
* attach`. Default off (it is a remote-control surface). When on, `dev`/`start` generate a
|
|
29
29
|
* per-boot bearer token and write `<stateRoot>/control.json` for local discovery. The serve
|
|
30
30
|
* binds all interfaces by default, so the routes are LAN-reachable with the token as the only
|
|
31
31
|
* protection — bind loopback (`--bind 127.0.0.1`; not `http.host`, which travels into a deployed
|
|
32
|
-
* image), firewall the port, or wrap it for real exposure (design §14).
|
|
32
|
+
* image), firewall the port, or wrap it for real exposure (docs/design/session-control.md §14).
|
|
33
33
|
*/
|
|
34
34
|
sessionControl?: boolean;
|
|
35
35
|
/** Deploy-time declarations for what the agent needs on the box, so real agents don't hand-write a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
3
4
|
import type { Agent } from "../../agent.ts";
|
|
4
5
|
import { type FastagentConfig } from "./config.ts";
|
|
@@ -7,7 +8,6 @@ import type { ModuleLoadFailure } from "../../loader.ts";
|
|
|
7
8
|
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
8
9
|
import { type PiAgentSessionFactory } from "./invoke-session.ts";
|
|
9
10
|
import { type AnyModel } from "./models.ts";
|
|
10
|
-
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { type PiSessionRecordStore } from "./session-store.ts";
|
|
12
12
|
import { type Lease, type SessionObserver } from "./turn-kit.ts";
|
|
13
13
|
/**
|
|
@@ -54,19 +54,14 @@ export interface AssembleSystemPromptOptions {
|
|
|
54
54
|
path: string;
|
|
55
55
|
content: string;
|
|
56
56
|
}>;
|
|
57
|
-
/** ③ Skills for the <available_skills> listing. */
|
|
58
|
-
skills?: Skill[];
|
|
59
|
-
/** ④ Env context, caller-provided (keeps this function pure). Omitted = segment omitted. */
|
|
60
|
-
cwd?: string;
|
|
61
57
|
}
|
|
62
58
|
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
63
59
|
/**
|
|
64
|
-
*
|
|
65
|
-
* the
|
|
66
|
-
*
|
|
67
|
-
* registry. Called synchronously, exactly once, before the agent is returned. Not public surface.
|
|
60
|
+
* The assembly, as a value: what every rung builds and what the agent runs on. The opener hands it to
|
|
61
|
+
* the control plane too — boundary mutations contend on the SAME lease and validate against the SAME
|
|
62
|
+
* registry the runs use, which is only true if there is one of each to hand over.
|
|
68
63
|
*/
|
|
69
|
-
export
|
|
64
|
+
export interface PiAssembly {
|
|
70
65
|
lease: Lease;
|
|
71
66
|
sessionFactory: PiAgentSessionFactory;
|
|
72
67
|
/** The registry and configured model, resolved on first use (a credential read is async). */
|
|
@@ -76,8 +71,9 @@ export type PiAssemblyParts = {
|
|
|
76
71
|
}>;
|
|
77
72
|
/** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
|
|
78
73
|
thinkingLevel: ThinkingLevel;
|
|
79
|
-
}
|
|
80
|
-
|
|
74
|
+
}
|
|
75
|
+
/** The agent an assembly runs as: the L0 over its lease and session factory. */
|
|
76
|
+
export declare function agentOf(assembly: PiAssembly, observer?: SessionObserver): Agent;
|
|
81
77
|
/** L1 options. Tier 1: model (spec) + instructions + tools. Tier 2: the injectable ports. */
|
|
82
78
|
export interface CreatePiAgentOptions {
|
|
83
79
|
/** Model spec "provider/modelId" (e.g. "openai-codex/gpt-5.5"), resolved against {@link models}. */
|
|
@@ -87,16 +83,15 @@ export interface CreatePiAgentOptions {
|
|
|
87
83
|
/**
|
|
88
84
|
* The system prompt itself — no engine base and no wrapping (unlike the directory path, which
|
|
89
85
|
* assembles the engine base + AGENTS.md as segment ② + persona.md as segment ①). A plain string or
|
|
90
|
-
* a factory
|
|
86
|
+
* a factory evaluated once per invoke, never during construction. Pi appends the skills listing
|
|
87
|
+
* when read is active.
|
|
91
88
|
*
|
|
92
89
|
* Not byte-for-byte verbatim: pi appends its own `Current working directory:` line to whatever
|
|
93
90
|
* prompt it is given. What this rung guarantees is that no engine IDENTITY is imposed — a
|
|
94
91
|
* hand-built agent is not told it is a coding assistant.
|
|
95
92
|
*/
|
|
96
93
|
instructions?: string | (() => string);
|
|
97
|
-
/** The tool set to mount
|
|
98
|
-
* {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
|
|
99
|
-
* coding tools — those bind their workspace at construction rather than reading a turn context. */
|
|
94
|
+
/** The tool set to mount: authored tools or pi's cwd-bound coding tools, both AgentTool. */
|
|
100
95
|
tools?: MountedTool[];
|
|
101
96
|
skills?: Skill[];
|
|
102
97
|
/**
|
|
@@ -114,12 +109,9 @@ export interface CreatePiAgentOptions {
|
|
|
114
109
|
/** Session persistence. Defaults to in-memory; inject piSessionRecordStore for restart-surviving
|
|
115
110
|
* continuity. */
|
|
116
111
|
sessions?: PiSessionRecordStore;
|
|
117
|
-
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* built for) or author-written `tools/`, which are code and can import anything. Not a
|
|
121
|
-
* sandbox — see {@link createPiAgentFromDefinition} for the rung where it also reads the
|
|
122
|
-
* definition. */
|
|
112
|
+
/** Supplies the working directory at L1 (default: process.cwd()), which loads no definition.
|
|
113
|
+
* At L2 it also reads persona.md and skills/.
|
|
114
|
+
* Tools and project-context discovery use the local process directly; this is not a sandbox. */
|
|
123
115
|
env?: ExecutionEnv;
|
|
124
116
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
125
117
|
lease?: Lease;
|
|
@@ -168,15 +160,18 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
168
160
|
lease?: Lease;
|
|
169
161
|
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|
|
170
162
|
observer?: SessionObserver;
|
|
171
|
-
/** INTERNAL seam for hub wiring; see {@link OnAssembly}. */
|
|
172
|
-
onAssembly?: OnAssembly;
|
|
173
163
|
}
|
|
174
164
|
/**
|
|
175
|
-
* L2
|
|
176
|
-
*
|
|
165
|
+
* L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. Returns the
|
|
166
|
+
* definition so callers can surface diagnostics/collisions. The opener consumes this form because
|
|
167
|
+
* the control plane needs the parts; {@link createPiAgentFromDefinition} is it plus the L0.
|
|
177
168
|
*/
|
|
169
|
+
export declare function assemblePiFromDefinition(dir: string, options: Omit<CreatePiAgentFromDefinitionOptions, "observer">): Promise<{
|
|
170
|
+
assembly: PiAssembly;
|
|
171
|
+
definition: LoadedDefinition;
|
|
172
|
+
}>;
|
|
173
|
+
/** L2: "point at a directory → agent": {@link assemblePiFromDefinition} under the L0. */
|
|
178
174
|
export declare function createPiAgentFromDefinition(dir: string, options: CreatePiAgentFromDefinitionOptions): Promise<{
|
|
179
175
|
agent: Agent;
|
|
180
176
|
definition: LoadedDefinition;
|
|
181
177
|
}>;
|
|
182
|
-
export {};
|