@fastagent-sh/fastagent 0.16.2 → 0.17.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/dist/channels/agentcore.js +24 -5
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +2 -1
- package/dist/cli/commands/attach.d.ts +18 -0
- package/dist/cli/commands/attach.js +46 -2
- package/dist/cli/commands/dev.js +2 -2
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/start.js +2 -2
- package/dist/cli/program.js +2 -2
- package/dist/deploy/agentcore/logs.d.ts +10 -5
- package/dist/deploy/agentcore/logs.js +2 -5
- package/dist/deploy/agentcore/plan.d.ts +3 -2
- package/dist/deploy/agentcore/plan.js +6 -5
- package/dist/engines/pi/create.js +7 -16
- package/dist/engines/pi/definition.d.ts +15 -0
- package/dist/engines/pi/definition.js +22 -1
- package/dist/engines/pi/open.d.ts +1 -1
- package/dist/engines/pi/open.js +19 -0
- package/dist/engines/pi/report.d.ts +16 -0
- package/dist/engines/pi/report.js +30 -0
- package/dist/engines/pi/session-builder.js +2 -2
- package/dist/engines/pi/session-control.d.ts +11 -1
- package/dist/engines/pi/session-control.js +3 -0
- package/dist/session-remote.js +17 -0
- package/dist/session.d.ts +20 -0
- package/package.json +1 -1
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - `{ kind: "invoke", session, text }` — the programmatic data plane; streams the invoke back as
|
|
20
20
|
* SSE (AgentCore's streaming response form), reusing the HTTP channel's handler wholesale.
|
|
21
21
|
*
|
|
22
|
-
* `/ping` reports `HealthyBusy`
|
|
22
|
+
* `/ping` reports `HealthyBusy` (+ `time_of_last_update`, required — see the handler) while
|
|
23
|
+
* process-wide background work is in flight (busy.ts) — webhook
|
|
23
24
|
* channels ACK fast and run turns fire-and-forget, and AgentCore ends an idle session, so without
|
|
24
25
|
* this signal a long turn would be killed mid-flight right after its ACK. `Healthy` when idle lets
|
|
25
26
|
* the platform reclaim the microVM (that idle-to-zero IS the point of this deployment).
|
|
@@ -242,12 +243,30 @@ export function agentcoreRoutes(options) {
|
|
|
242
243
|
stateSync.save();
|
|
243
244
|
return response;
|
|
244
245
|
};
|
|
246
|
+
// The Runtime ping contract: Healthy = reclaimable, HealthyBusy = keep the session alive
|
|
247
|
+
// (background turns in flight). `time_of_last_update` is REQUIRED for the keep-alive to work,
|
|
248
|
+
// despite the contract documenting it as optional ("If you omit the field, the platform tracks
|
|
249
|
+
// status changes on its own"): measured on a live Runtime (us-east-1, 2026-08-04), the platform's
|
|
250
|
+
// idle measurement reads ONLY this field — with it omitted, a session polling every ~2s and
|
|
251
|
+
// receiving HealthyBusy 200s was still reclaimed at exactly IdleRuntimeSessionTimeout after the
|
|
252
|
+
// last InvokeAgentRuntime, mid-turn, 2s after the last HealthyBusy answer; with the field present
|
|
253
|
+
// the same turn survived 3.5× the idle timeout with zero invocations and completed. The value
|
|
254
|
+
// updates ONLY on a real status change: a timestamp advancing on every ping declares a perpetual
|
|
255
|
+
// status change, so the idle timeout never fires and dead-idle sessions live to MaxLifetime
|
|
256
|
+
// (quota exhaustion — the failure mode the contract's warning describes).
|
|
257
|
+
let lastStatus = "Healthy";
|
|
258
|
+
let lastTransition = Math.floor(Date.now() / 1000);
|
|
245
259
|
return {
|
|
246
260
|
"POST /invocations": invocations,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
261
|
+
"GET /ping": () => {
|
|
262
|
+
const status = isBusy() ? "HealthyBusy" : "Healthy";
|
|
263
|
+
if (status !== lastStatus) {
|
|
264
|
+
lastTransition = Math.floor(Date.now() / 1000);
|
|
265
|
+
log.debug(`[agentcore] ping status: ${lastStatus} → ${status}`);
|
|
266
|
+
lastStatus = status;
|
|
267
|
+
}
|
|
268
|
+
return json({ status, time_of_last_update: lastTransition }, 200);
|
|
269
|
+
},
|
|
251
270
|
};
|
|
252
271
|
}
|
|
253
272
|
/** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
|
|
@@ -22,7 +22,7 @@ export interface ControlRoutesOptions {
|
|
|
22
22
|
agent?: Agent;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Mount the control plane: `GET /control/capabilities|state|entries|events` + `POST
|
|
25
|
+
* Mount the control plane: `GET /control/capabilities|commands|state|entries|events` + `POST
|
|
26
26
|
* /control/dispatch`, all bearer-authenticated. `events` streams SSE (`data: <WireEvent>` lines).
|
|
27
27
|
*/
|
|
28
28
|
export declare function controlRoutes(control: SessionControl, options: ControlRoutesOptions): Routes;
|
package/dist/channels/control.js
CHANGED
|
@@ -84,7 +84,7 @@ function parseWireCommand(raw) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* Mount the control plane: `GET /control/capabilities|state|entries|events` + `POST
|
|
87
|
+
* Mount the control plane: `GET /control/capabilities|commands|state|entries|events` + `POST
|
|
88
88
|
* /control/dispatch`, all bearer-authenticated. `events` streams SSE (`data: <WireEvent>` lines).
|
|
89
89
|
*/
|
|
90
90
|
export function controlRoutes(control, options) {
|
|
@@ -111,6 +111,7 @@ export function controlRoutes(control, options) {
|
|
|
111
111
|
return {
|
|
112
112
|
...(invokeHandler ? { "POST /control/invoke": guard((req) => invokeHandler(req)) } : {}),
|
|
113
113
|
"GET /control/capabilities": guard(() => json(control.capabilities())),
|
|
114
|
+
"GET /control/commands": guard(async () => json(await control.commands())),
|
|
114
115
|
"GET /control/state": guard(async (_req, url) => {
|
|
115
116
|
const session = sessionParam(url);
|
|
116
117
|
if (!session)
|
|
@@ -72,6 +72,24 @@ export interface AttachIo {
|
|
|
72
72
|
* whole — the one case where "unknown" must not read as "off-path".
|
|
73
73
|
*/
|
|
74
74
|
export declare function activePathSlice(entries: SessionEntry[], leafEntryId: string | undefined): SessionEntry[];
|
|
75
|
+
/**
|
|
76
|
+
* Answer a RESERVED-SLASH line (anything starting with `/` that is not `/abort`). Two intents share
|
|
77
|
+
* the prefix — a mistyped control command and an attempt to invoke a name — and they are answered
|
|
78
|
+
* differently:
|
|
79
|
+
*
|
|
80
|
+
* - a mistyped slash gets the certain half NOW (a leading `/` is reserved here, whatever the token
|
|
81
|
+
* turns out to be), because waiting on a remote read that can be slow or fail would leave the
|
|
82
|
+
* input unanswered; it deliberately does not pre-judge the token as unknown — the read may be
|
|
83
|
+
* about to prove it names a real skill;
|
|
84
|
+
* - `/commands` prints nothing first: its whole answer IS the read, and a placeholder is noise;
|
|
85
|
+
* - the enumeration answers `/commands` ONLY — dumping every skill at a mistyped `/aboort` answers
|
|
86
|
+
* an intent the typo did not express.
|
|
87
|
+
*
|
|
88
|
+
* Names print BARE: this composer cannot expand `/name` (the data plane takes prompts as text), so
|
|
89
|
+
* printing them with a slash would invite the user straight back into this branch. Returns the
|
|
90
|
+
* promise for the remote half, so a caller (or a test) can await the second line.
|
|
91
|
+
*/
|
|
92
|
+
export declare function answerSlashInput(trimmed: string, control: Pick<SessionControl, "commands">, println: (line: string) => void): Promise<void>;
|
|
75
93
|
/**
|
|
76
94
|
* ONE attach round: subscribe → backfill (render the durable record since `cursor`) → drain live
|
|
77
95
|
* until the stream drops. Returns the advanced cursor. Subscribing first + the server's eager
|
|
@@ -185,7 +185,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
185
185
|
// invoke) — give the human a corrective signal.
|
|
186
186
|
log.warn(`[fastagent] no durable record for "${sessionArg}" yet — a new session, or a typo?`);
|
|
187
187
|
}
|
|
188
|
-
log.info(`[fastagent] type to steer the active run; /abort to stop it; Ctrl+C to detach`);
|
|
188
|
+
log.info(`[fastagent] type to steer the active run; /abort to stop it; /commands to list what this agent defines; Ctrl+C to detach`);
|
|
189
189
|
// stdin → the two planes: a line steers the ACTIVE run; with no run to join (no_active_run) it
|
|
190
190
|
// falls back to STARTING one over the remote data plane (`POST /control/invoke`) — try-steer-
|
|
191
191
|
// then-prompt avoids a state() pre-check race. Acceptance is not outcome: rejections print and
|
|
@@ -222,7 +222,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
222
222
|
// `/` is a reserved command prefix: a typo'd /aboort silently steering the model (injecting a
|
|
223
223
|
// prompt when the user meant to STOP the run) is the dangerous direction of the ambiguity.
|
|
224
224
|
if (trimmed.startsWith("/") && trimmed !== "/abort") {
|
|
225
|
-
|
|
225
|
+
void answerSlashInput(trimmed, control, (l) => console.log(l));
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
const command = trimmed === "/abort" ? { type: "abort" } : { type: "steer", prompt: { text: trimmed } };
|
|
@@ -449,6 +449,50 @@ export function activePathSlice(entries, leafEntryId) {
|
|
|
449
449
|
}
|
|
450
450
|
return entries.filter((e) => onPath.has(e.id));
|
|
451
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Answer a RESERVED-SLASH line (anything starting with `/` that is not `/abort`). Two intents share
|
|
454
|
+
* the prefix — a mistyped control command and an attempt to invoke a name — and they are answered
|
|
455
|
+
* differently:
|
|
456
|
+
*
|
|
457
|
+
* - a mistyped slash gets the certain half NOW (a leading `/` is reserved here, whatever the token
|
|
458
|
+
* turns out to be), because waiting on a remote read that can be slow or fail would leave the
|
|
459
|
+
* input unanswered; it deliberately does not pre-judge the token as unknown — the read may be
|
|
460
|
+
* about to prove it names a real skill;
|
|
461
|
+
* - `/commands` prints nothing first: its whole answer IS the read, and a placeholder is noise;
|
|
462
|
+
* - the enumeration answers `/commands` ONLY — dumping every skill at a mistyped `/aboort` answers
|
|
463
|
+
* an intent the typo did not express.
|
|
464
|
+
*
|
|
465
|
+
* Names print BARE: this composer cannot expand `/name` (the data plane takes prompts as text), so
|
|
466
|
+
* printing them with a slash would invite the user straight back into this branch. Returns the
|
|
467
|
+
* promise for the remote half, so a caller (or a test) can await the second line.
|
|
468
|
+
*/
|
|
469
|
+
export async function answerSlashInput(trimmed, control, println) {
|
|
470
|
+
// The first WORD is the token: slash input naturally carries arguments (`/triage my inbox`), and
|
|
471
|
+
// taking the whole line would answer "names nothing" for a name the user did give.
|
|
472
|
+
const word = trimmed.slice(1).split(/\s+/)[0] ?? "";
|
|
473
|
+
const listing = word === "commands";
|
|
474
|
+
if (!listing)
|
|
475
|
+
println("[a leading / is reserved — /abort stops the run, /commands lists what this agent defines]");
|
|
476
|
+
let commands;
|
|
477
|
+
try {
|
|
478
|
+
commands = await control.commands();
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
println(`[command list unavailable: ${error}]`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (listing) {
|
|
485
|
+
// `description` is what makes a listing usable — a bare name tells the author nothing they did
|
|
486
|
+
// not already know from the directory.
|
|
487
|
+
const listed = commands.map((c) => (c.description ? `${c.name} — ${c.description}` : c.name));
|
|
488
|
+
println(listed.length ? `[this agent defines: ${listed.join("; ")}]` : "[this agent defines no names]");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const hit = commands.find((c) => c.name === word);
|
|
492
|
+
println(hit
|
|
493
|
+
? `[${hit.name} is a ${hit.source}${hit.description ? ` — ${hit.description}` : ""}; name it in a normal message, without the /]`
|
|
494
|
+
: `[/${word} names nothing this agent defines]`);
|
|
495
|
+
}
|
|
452
496
|
/**
|
|
453
497
|
* ONE attach round: subscribe → backfill (render the durable record since `cursor`) → drain live
|
|
454
498
|
* until the stream drops. Returns the advanced cursor. Subscribing first + the server's eager
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { runDevSupervisor } from "../../dev-supervisor.js";
|
|
8
8
|
import { loadDotEnv } from "../../env.js";
|
|
9
|
-
import {
|
|
9
|
+
import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
|
|
10
10
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
11
11
|
import { setLogLevel } from "../../log.js";
|
|
12
12
|
import { logAgentLoop } from "../../observe.js";
|
|
@@ -91,5 +91,5 @@ function reportAgentsSkillsTools(a) {
|
|
|
91
91
|
}
|
|
92
92
|
reportToolCollisions(a.toolCollisions);
|
|
93
93
|
reportModuleLoadFailures(a.toolFailures);
|
|
94
|
-
|
|
94
|
+
reportFindingsIfChanged(a.definition.dir, a.definition);
|
|
95
95
|
}
|
|
@@ -6,7 +6,7 @@ import { defaultSessionsDir, loadConfig, resolveAuthPath, resolveModelSpec, reso
|
|
|
6
6
|
import { resolveStateRoot, workspaceHint } from "../../paths.js";
|
|
7
7
|
import { resolveAgentTools } from "../../engines/pi/create.js";
|
|
8
8
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
9
|
-
import {
|
|
9
|
+
import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
|
|
10
10
|
import { log } from "../../log.js";
|
|
11
11
|
import { nextRun } from "../../schedule/cron.js";
|
|
12
12
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
@@ -111,5 +111,5 @@ export async function runInfo(dirArg, opts) {
|
|
|
111
111
|
reportModuleLoadFailures(sched.failures);
|
|
112
112
|
if (tools.error)
|
|
113
113
|
log.warn(`[fastagent] ${tools.error}`);
|
|
114
|
-
|
|
114
|
+
reportFindingsIfChanged(definition.dir, definition);
|
|
115
115
|
}
|
|
@@ -9,7 +9,7 @@ import { loadDotEnv } from "../../env.js";
|
|
|
9
9
|
import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
|
|
10
10
|
import { resolveSecretsDir, workspaceHint } from "../../paths.js";
|
|
11
11
|
import { isUnderDir } from "../../engines/pi/definition.js";
|
|
12
|
-
import {
|
|
12
|
+
import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
|
|
13
13
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
14
14
|
import { log, setLogLevel } from "../../log.js";
|
|
15
15
|
import { createWakeAlarmSink, reconcileWakeAlarms } from "../../schedule/wake-alarm.js";
|
|
@@ -80,7 +80,7 @@ export async function runStart(dirArg, opts) {
|
|
|
80
80
|
log.info(`[fastagent] note: secrets (.env, rotated auth.json) live under the definition dir; point ` +
|
|
81
81
|
`FASTAGENT_SECRETS_DIR at a persistent volume so a redeploy that replaces the dir does not wipe them.`);
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
reportFindingsIfChanged(definition.dir, definition);
|
|
84
84
|
// AgentCore Runtime posture (FASTAGENT_AGENTCORE=1, set by the generated deploy artifacts): the
|
|
85
85
|
// adapter (POST /invocations + GET /ping) is the container's only reachable surface, and cron
|
|
86
86
|
// slots arrive from the external clock through it — so no resident cron timers. In particular,
|
package/dist/cli/program.js
CHANGED
|
@@ -457,8 +457,8 @@ const logs = {
|
|
|
457
457
|
name: "logs",
|
|
458
458
|
summary: "find and tail a deployed host's application logs",
|
|
459
459
|
description: "Find the CloudWatch log group for the AgentCore stack derived from dir, then run aws logs tail. " +
|
|
460
|
-
"The default Runtime source
|
|
461
|
-
"
|
|
460
|
+
"The default Runtime source shows the agent process's own stdout/stderr; the forwarder source shows " +
|
|
461
|
+
"the Lambda ingress transport logs.",
|
|
462
462
|
args: [{ name: "<host>", description: "deployed host", choices: ["agentcore"] }, DIR_ARG],
|
|
463
463
|
flags: [
|
|
464
464
|
{ flags: "--source <source>", description: "agentcore log source: runtime (default) or forwarder" },
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgentCore log discovery + tailing.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* AgentCore log discovery + tailing. The container's stdout/stderr lives in a per-endpoint CloudWatch
|
|
3
|
+
* log group, while the forwarder Lambda has a separate group. This operator surface resolves the
|
|
4
|
+
* stack's RuntimeArn, discovers the endpoint group by prefix, and tails it.
|
|
5
|
+
*
|
|
6
|
+
* NO STREAM FILTER, deliberately: AgentCore names its streams `YYYY/MM/DD/[runtime-logs]<session-id>`
|
|
7
|
+
* (the Lambda `2024/01/01/[$LATEST]abc` convention), so `[runtime-logs]` is an INFIX after the UTC date
|
|
8
|
+
* path, not a prefix. `--log-stream-name-prefix` is a literal prefix match, and the AWS CLI has no
|
|
9
|
+
* substring filter (`--log-stream-names` takes exact names, which `--follow` could never extend to the
|
|
10
|
+
* new session streams). Passing the marker as a prefix therefore matches zero streams and `aws logs
|
|
11
|
+
* tail` prints nothing and exits 0 — a silent empty tail. Do not add it back.
|
|
7
12
|
*/
|
|
8
13
|
import type { CliRunner } from "../runner.ts";
|
|
9
14
|
export type AgentcoreLogSource = "runtime" | "forwarder";
|
|
@@ -93,9 +93,8 @@ export async function tailAgentcoreLogs(plan, aws, announce = () => { }) {
|
|
|
93
93
|
if (matches.length > 1) {
|
|
94
94
|
return {
|
|
95
95
|
ok: false,
|
|
96
|
-
gate: `several Runtime log groups match this stack: ${matches.join(", ")} — tail the intended one
|
|
97
|
-
`
|
|
98
|
-
`(quote the prefix — [...] is a shell glob)`,
|
|
96
|
+
gate: `several Runtime log groups match this stack: ${matches.join(", ")} — tail the intended one directly: ` +
|
|
97
|
+
`aws logs tail <group> --format short --follow`,
|
|
99
98
|
};
|
|
100
99
|
}
|
|
101
100
|
const logGroup = matches[0];
|
|
@@ -103,8 +102,6 @@ export async function tailAgentcoreLogs(plan, aws, announce = () => { }) {
|
|
|
103
102
|
const tailArgs = ["logs", "tail", logGroup, "--format", "short"];
|
|
104
103
|
if (plan.since)
|
|
105
104
|
tailArgs.push("--since", plan.since);
|
|
106
|
-
if (plan.source === "runtime")
|
|
107
|
-
tailArgs.push("--log-stream-name-prefix", "[runtime-logs]");
|
|
108
105
|
if (plan.follow)
|
|
109
106
|
tailArgs.push("--follow");
|
|
110
107
|
const tailed = await aws(tailArgs);
|
|
@@ -63,8 +63,9 @@ export declare const SECRETS_DIR = "/mnt/state/.secrets";
|
|
|
63
63
|
* — idle included, at the peak level reached — so this tail is the standing cost of every burst of
|
|
64
64
|
* activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
|
|
65
65
|
* platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
|
|
66
|
-
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy
|
|
67
|
-
*
|
|
66
|
+
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy + time_of_last_update while
|
|
67
|
+
* work is in flight (the FIELD is what the platform's idle measurement actually reads — agentcore.ts),
|
|
68
|
+
* so this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
|
|
68
69
|
* AWS accepts 60–28800.
|
|
69
70
|
*/
|
|
70
71
|
export declare const IDLE_TIMEOUT_SECONDS = 180;
|
|
@@ -62,8 +62,9 @@ export const SECRETS_DIR = `${MOUNT}/${SECRETS_DIRNAME}`;
|
|
|
62
62
|
* — idle included, at the peak level reached — so this tail is the standing cost of every burst of
|
|
63
63
|
* activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
|
|
64
64
|
* platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
|
|
65
|
-
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy
|
|
66
|
-
*
|
|
65
|
+
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy + time_of_last_update while
|
|
66
|
+
* work is in flight (the FIELD is what the platform's idle measurement actually reads — agentcore.ts),
|
|
67
|
+
* so this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
|
|
67
68
|
* AWS accepts 60–28800.
|
|
68
69
|
*/
|
|
69
70
|
export const IDLE_TIMEOUT_SECONDS = 180;
|
|
@@ -562,7 +563,7 @@ function template(input, translated) {
|
|
|
562
563
|
` # forces a NAT gateway for model/channel egress (~$33/mo) — deliberately not the default.`,
|
|
563
564
|
` FilesystemConfigurations:`,
|
|
564
565
|
` - SessionStorage: { MountPath: ${MOUNT} }`,
|
|
565
|
-
` # Idle ${IDLE_TIMEOUT_SECONDS}s (the ping's HealthyBusy keeps BUSY sessions alive
|
|
566
|
+
` # Idle ${IDLE_TIMEOUT_SECONDS}s (the ping's HealthyBusy + time_of_last_update keeps BUSY sessions alive), max compute`,
|
|
566
567
|
` # lifetime ${MAX_LIFETIME_SECONDS}s — the platform ceiling; the session id stays valid, so the next invoke`,
|
|
567
568
|
` # just gets fresh compute with the same storage. Memory bills per second for the whole`,
|
|
568
569
|
` # session INCLUDING the idle tail, so a shorter tail is the main cost lever here.`,
|
|
@@ -712,7 +713,7 @@ export function planAgentcoreDeploy(input) {
|
|
|
712
713
|
? `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`
|
|
713
714
|
: `# 4. Read the outputs (the runtime ARN — this topology has NO public URL: nothing outside AWS`, ...(needsFunctionUrl
|
|
714
715
|
? []
|
|
715
|
-
: [`# sends to it, so no Function URL is created and the agent is reachable only via SigV4).`]), `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery
|
|
716
|
+
: [`# sends to it, so no Function URL is created and the agent is reachable only via SigV4).`]), `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, ...(needsForwarder
|
|
716
717
|
? [
|
|
717
718
|
`# The ingress transport is a separate Lambda and therefore a separate log source:`,
|
|
718
719
|
`fastagent logs agentcore --source forwarder --follow`,
|
|
@@ -732,7 +733,7 @@ export function planAgentcoreDeploy(input) {
|
|
|
732
733
|
post.push(`# Register the Telegram webhook (default route POST /telegram; secret_token MUST equal TELEGRAM_SECRET_TOKEN):`, `curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`, ` -d url=<ForwarderUrl>/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`);
|
|
733
734
|
}
|
|
734
735
|
if (channels.includes("github")) {
|
|
735
|
-
post.push(`# Set the GitHub webhook (repo Settings → Webhooks): Payload URL = <ForwarderUrl>/webhook,`, `# content type application/json, secret = GITHUB_WEBHOOK_SECRET.`, `# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy holds the session while turns run, but the 8 h compute ceiling is hard).`);
|
|
736
|
+
post.push(`# Set the GitHub webhook (repo Settings → Webhooks): Payload URL = <ForwarderUrl>/webhook,`, `# content type application/json, secret = GITHUB_WEBHOOK_SECRET.`, `# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy + time_of_last_update holds the session while turns run, but the 8 h compute ceiling is hard).`);
|
|
736
737
|
}
|
|
737
738
|
if (channels.includes("slack")) {
|
|
738
739
|
post.push(`# Set Slack Event Subscriptions → Request URL = <ForwarderUrl>/slack (scopes per channels/slack.ts).`);
|
|
@@ -19,7 +19,7 @@ import { resolveSecretsDir } from "../../paths.js";
|
|
|
19
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
20
20
|
import { DEFAULT_THINKING_LEVEL, piHarnessFactory } from "./harness.js";
|
|
21
21
|
import { createPiModels } from "./models.js";
|
|
22
|
-
import {
|
|
22
|
+
import { reportFindingsIfChanged } from "./report.js";
|
|
23
23
|
import { inMemorySessionStore } from "./sessions.js";
|
|
24
24
|
import { isDeferredTool, loadTools, mergeDiscoveredTools, } from "./tool.js";
|
|
25
25
|
import { withSearchTool } from "./search-tools.js";
|
|
@@ -221,12 +221,6 @@ export function createPiAgent(options) {
|
|
|
221
221
|
observer: options.observer,
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
|
-
/** Stable identity of a definition's non-fatal findings, for change-detection in `live` (dedup only). */
|
|
225
|
-
function findingsSignature(def) {
|
|
226
|
-
const collisions = def.collisions.map((c) => `c:${c.name}:${c.winnerPath}:${c.loserPath}`);
|
|
227
|
-
const diagnostics = def.diagnostics.map((d) => `d:${d.code}:${d.path}`);
|
|
228
|
-
return [...collisions, ...diagnostics].sort().join("\n");
|
|
229
|
-
}
|
|
230
224
|
/**
|
|
231
225
|
* L2: "point at a directory → agent": load + assemble (base + AGENTS.md + skills + env) + L1 in one
|
|
232
226
|
* call. Returns the definition so callers can surface diagnostics/collisions.
|
|
@@ -239,10 +233,11 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
239
233
|
// Boot-time load: fail-visibly at startup on a broken directory, and give callers the snapshot to
|
|
240
234
|
// report (skills/diagnostics/collisions). Serving does NOT close over it — see `live` below.
|
|
241
235
|
const definition = await loadAgentDefinition(dir, { cwd: env.cwd, env });
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
236
|
+
// Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by
|
|
237
|
+
// the resolved dir): announced once here, and re-announced by a turn or by the control plane's
|
|
238
|
+
// command list only when the set CHANGES — a runtime-written bad skill surfaces the moment it
|
|
239
|
+
// appears, a static one does not spam. Log dedup, not session state (stateless invoke holds).
|
|
240
|
+
reportFindingsIfChanged(definition.dir, definition);
|
|
246
241
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
247
242
|
// it; a caller's own search_tools wins).
|
|
248
243
|
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
@@ -265,11 +260,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
265
260
|
// next good edit heals both.
|
|
266
261
|
live: async () => {
|
|
267
262
|
const def = await loadAgentDefinition(dir, { cwd: env.cwd, env });
|
|
268
|
-
|
|
269
|
-
if (sig !== reportedFindings) {
|
|
270
|
-
reportedFindings = sig;
|
|
271
|
-
reportDefinitionWarnings(def.collisions, def.diagnostics);
|
|
272
|
-
}
|
|
263
|
+
reportFindingsIfChanged(def.dir, def);
|
|
273
264
|
return {
|
|
274
265
|
systemPrompt: assembleSystemPrompt({
|
|
275
266
|
// Segment ①: an authored persona (persona.md, def.persona) overrides the engine identity,
|
|
@@ -41,6 +41,21 @@ export interface LoadAgentDefinitionOptions {
|
|
|
41
41
|
}
|
|
42
42
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
43
43
|
export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
|
|
44
|
+
/**
|
|
45
|
+
* The definition's skills ALONE, resolved the same way `loadAgentDefinition` resolves them (same
|
|
46
|
+
* loader, same containment guard, same first-wins collision rule) — for readers that need only the
|
|
47
|
+
* names and must not pay the full load's ② context walk (every AGENTS.md from cwd to root) for them.
|
|
48
|
+
* The control plane's `commands()` is that reader, called when a composer opens its completion list.
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadAgentSkills(agentDir: string, options?: {
|
|
51
|
+
cwd?: string;
|
|
52
|
+
env?: ExecutionEnv;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
skills: Skill[];
|
|
55
|
+
diagnostics: SkillDiagnostic[];
|
|
56
|
+
collisions: SkillCollision[];
|
|
57
|
+
dir: string;
|
|
58
|
+
}>;
|
|
44
59
|
/**
|
|
45
60
|
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
46
61
|
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
@@ -42,6 +42,11 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
42
42
|
throw new Error(`cannot read ${personaPath}: ${personaRead.error.message}`);
|
|
43
43
|
}
|
|
44
44
|
const persona = personaRead.ok ? personaRead.value : undefined;
|
|
45
|
+
const { skills, diagnostics, collisions } = await readSkills(e, root);
|
|
46
|
+
return { contextFiles, persona, skills, diagnostics, collisions, dir: root };
|
|
47
|
+
}
|
|
48
|
+
/** The skills half, shared by the full load and {@link loadAgentSkills}. `root` is already resolved. */
|
|
49
|
+
async function readSkills(e, root) {
|
|
45
50
|
// Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
|
|
46
51
|
// definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
|
|
47
52
|
// that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
|
|
@@ -58,7 +63,23 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
58
63
|
byName.set(skill.name, skill);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
|
-
return {
|
|
66
|
+
return { skills: [...byName.values()], diagnostics, collisions };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The definition's skills ALONE, resolved the same way `loadAgentDefinition` resolves them (same
|
|
70
|
+
* loader, same containment guard, same first-wins collision rule) — for readers that need only the
|
|
71
|
+
* names and must not pay the full load's ② context walk (every AGENTS.md from cwd to root) for them.
|
|
72
|
+
* The control plane's `commands()` is that reader, called when a composer opens its completion list.
|
|
73
|
+
*/
|
|
74
|
+
export async function loadAgentSkills(agentDir, options = {}) {
|
|
75
|
+
const cwd = options.cwd ?? agentDir;
|
|
76
|
+
const e = options.env ?? new NodeExecutionEnv({ cwd });
|
|
77
|
+
const rootResult = await e.absolutePath(agentDir);
|
|
78
|
+
if (!rootResult.ok)
|
|
79
|
+
throw new Error(`cannot resolve agent dir "${agentDir}": ${rootResult.error.message}`);
|
|
80
|
+
// `dir` is the RESOLVED root, like {@link LoadedDefinition.dir}: readers key per-definition state
|
|
81
|
+
// (the findings memo) on it, and "./agent" vs an absolute path must not become two definitions.
|
|
82
|
+
return { ...(await readSkills(e, rootResult.value)), dir: rootResult.value };
|
|
62
83
|
}
|
|
63
84
|
/**
|
|
64
85
|
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
@@ -4,7 +4,7 @@ import type { SessionControl } from "../../session.ts";
|
|
|
4
4
|
import type { SessionObserver } from "./invoke.ts";
|
|
5
5
|
import type { PiSessionReader, PiSessionStore } from "./sessions.ts";
|
|
6
6
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
7
|
-
import type
|
|
7
|
+
import { type LoadedDefinition } from "./definition.ts";
|
|
8
8
|
import type { ToolCollision } from "./tool.ts";
|
|
9
9
|
import type { MountedTool } from "./tool.ts";
|
|
10
10
|
export interface CreatePiAgentFromDirOptions {
|
package/dist/engines/pi/open.js
CHANGED
|
@@ -14,6 +14,8 @@ import { resolveStateRoot, resolvePlacement } from "../../paths.js";
|
|
|
14
14
|
import { createPiAgentFromDefinition, resolveAgentTools } from "./create.js";
|
|
15
15
|
import { createPiSessionControl } from "./session-control.js";
|
|
16
16
|
import { withWakeTool } from "./wake-tool.js";
|
|
17
|
+
import { loadAgentSkills } from "./definition.js";
|
|
18
|
+
import { reportFindingsIfChanged } from "./report.js";
|
|
17
19
|
import { jsonlSessionStore } from "./sessions.js";
|
|
18
20
|
export async function resolveAgentAssembly(dir, options = {}) {
|
|
19
21
|
// Placement is structural (resolvePlacement): the AGENT DIR carries definition + config + machinery;
|
|
@@ -73,6 +75,23 @@ export async function createPiAgentFromDir(dir, options = {}) {
|
|
|
73
75
|
? createPiSessionControl({
|
|
74
76
|
sessions,
|
|
75
77
|
boundary: () => boundaryParts,
|
|
78
|
+
// Skills ARE the names a client offers — the resolved set, after collisions were decided
|
|
79
|
+
// first-wins, which a client cannot reconstruct from the directory. Read LIVE (the directory
|
|
80
|
+
// is the agent: a skill added while serving is in play on the next turn, so it must be
|
|
81
|
+
// listable now) and SKILLS-ONLY: this is called when a composer opens its completion list,
|
|
82
|
+
// and the full load's ② context walk buys nothing here.
|
|
83
|
+
commands: async () => {
|
|
84
|
+
const loaded = await loadAgentSkills(agentDir, { cwd: workspace });
|
|
85
|
+
// A skill whose frontmatter broke simply is not in `skills` — it would disappear from the
|
|
86
|
+
// author's composer with no signal anywhere. The memo is SHARED with the turn path (keyed
|
|
87
|
+
// by dir), so a finding is warned when it appears, not once per reader that notices it.
|
|
88
|
+
reportFindingsIfChanged(loaded.dir, loaded);
|
|
89
|
+
return loaded.skills.map((skill) => ({
|
|
90
|
+
name: skill.name,
|
|
91
|
+
description: skill.description,
|
|
92
|
+
source: "skill",
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
76
95
|
// The caller tap's boundary-event half: state_changed/compaction_* originate in the hub
|
|
77
96
|
// and never cross the data plane's observer seam — without this, an audit tap wired here
|
|
78
97
|
// would miss exactly the mutations it most needs to see (set_model).
|
|
@@ -6,8 +6,24 @@ import type { SkillDiagnostic } from "@earendil-works/pi-agent-core";
|
|
|
6
6
|
import type { SkillCollision } from "./definition.ts";
|
|
7
7
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
8
8
|
import type { ToolCollision } from "./tool.ts";
|
|
9
|
+
type Findings = {
|
|
10
|
+
collisions: SkillCollision[];
|
|
11
|
+
diagnostics: SkillDiagnostic[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* THE door for definition findings: warns only when this dir's set CHANGED since the last report.
|
|
15
|
+
* Every reader calls it — boot, the per-turn live read, the control plane's command list — so a
|
|
16
|
+
* finding is announced when it appears and never repeated. There is deliberately no "record without
|
|
17
|
+
* printing" variant: a memo entry that trusts some other caller to have printed would silently
|
|
18
|
+
* swallow findings for a caller that does not.
|
|
19
|
+
*
|
|
20
|
+
* `dir` must be the RESOLVED definition root (`LoadedDefinition.dir`), or two spellings of one path
|
|
21
|
+
* become two memos and warn twice.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reportFindingsIfChanged(dir: string, def: Findings): void;
|
|
9
24
|
export declare function reportDefinitionWarnings(collisions: SkillCollision[], diagnostics: SkillDiagnostic[]): void;
|
|
10
25
|
export declare function reportToolCollisions(collisions: ToolCollision[]): void;
|
|
11
26
|
/** Report per-file module failures. The caller decides whether they are degradations (tools/schedules)
|
|
12
27
|
* or fatal (declared channels on the serving path). */
|
|
13
28
|
export declare function reportModuleLoadFailures(failures: ModuleLoadFailure[]): void;
|
|
29
|
+
export {};
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { log } from "../../log.js";
|
|
2
|
+
/** Stable identity of a definition's non-fatal findings — the dedup key below. */
|
|
3
|
+
function findingsSignature(def) {
|
|
4
|
+
const collisions = def.collisions.map((c) => `c:${c.name}:${c.winnerPath}:${c.loserPath}`);
|
|
5
|
+
const diagnostics = def.diagnostics.map((d) => `d:${d.code}:${d.path}`);
|
|
6
|
+
return [...collisions, ...diagnostics].sort().join("\n");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The last reported finding set PER DEFINITION DIR. One memo for every reader of a definition (the
|
|
10
|
+
* per-turn live read, the control plane's command list), because the thing being deduped is the
|
|
11
|
+
* FINDING, not the reader: a newly broken skill deserves one warning, not one per code path that
|
|
12
|
+
* noticed it, and a static one deserves none at all.
|
|
13
|
+
*/
|
|
14
|
+
const lastFindings = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* THE door for definition findings: warns only when this dir's set CHANGED since the last report.
|
|
17
|
+
* Every reader calls it — boot, the per-turn live read, the control plane's command list — so a
|
|
18
|
+
* finding is announced when it appears and never repeated. There is deliberately no "record without
|
|
19
|
+
* printing" variant: a memo entry that trusts some other caller to have printed would silently
|
|
20
|
+
* swallow findings for a caller that does not.
|
|
21
|
+
*
|
|
22
|
+
* `dir` must be the RESOLVED definition root (`LoadedDefinition.dir`), or two spellings of one path
|
|
23
|
+
* become two memos and warn twice.
|
|
24
|
+
*/
|
|
25
|
+
export function reportFindingsIfChanged(dir, def) {
|
|
26
|
+
const sig = findingsSignature(def);
|
|
27
|
+
if (lastFindings.get(dir) === sig)
|
|
28
|
+
return;
|
|
29
|
+
lastFindings.set(dir, sig);
|
|
30
|
+
reportDefinitionWarnings(def.collisions, def.diagnostics);
|
|
31
|
+
}
|
|
2
32
|
export function reportDefinitionWarnings(collisions, diagnostics) {
|
|
3
33
|
for (const c of collisions) {
|
|
4
34
|
log.warn(`[fastagent] skill "${c.name}" collision — using ${c.winnerPath}, ignoring ${c.loserPath}`);
|
|
@@ -37,7 +37,7 @@ import { canonicalPath, loadAgentDefinition } from "./definition.js";
|
|
|
37
37
|
import { createPiModelRuntime, probeAuthSource } from "./models.js";
|
|
38
38
|
import { log } from "../../log.js";
|
|
39
39
|
import { additiveActivation, turnContext } from "./tool-context.js";
|
|
40
|
-
import {
|
|
40
|
+
import { reportFindingsIfChanged, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
|
|
41
41
|
import { resolveAgentAssembly } from "./open.js";
|
|
42
42
|
/** Adapt coding-agent's resident SessionManager to FastAgent's shared tool-runtime manager port. */
|
|
43
43
|
function toolChatSessionManager(session) {
|
|
@@ -119,7 +119,7 @@ sessionManager) {
|
|
|
119
119
|
const model = resolveModel(modelRuntime, modelSpec);
|
|
120
120
|
const env = new NodeExecutionEnv({ cwd });
|
|
121
121
|
const definition = await loadAgentDefinition(agentDir, { cwd, env });
|
|
122
|
-
|
|
122
|
+
reportFindingsIfChanged(definition.dir, definition);
|
|
123
123
|
const defaultNames = piDefaultTools().map((t) => t.name);
|
|
124
124
|
const customTools = tools.filter((t) => !defaultNames.includes(t.name));
|
|
125
125
|
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Models } from "@earendil-works/pi-ai";
|
|
3
|
-
import { type SessionControl, type SessionEvent } from "../../session.ts";
|
|
3
|
+
import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
|
|
4
4
|
import type { Lease, SessionObserver } from "./invoke.ts";
|
|
5
5
|
import { type AnyModel, type PiHarnessFactory } from "./harness.ts";
|
|
6
6
|
import { type PiSessionReader } from "./sessions.ts";
|
|
@@ -39,6 +39,16 @@ export interface CreatePiSessionControlOptions {
|
|
|
39
39
|
* (assembly completes before any dispatch can arrive). Absent / undefined → boundary commands
|
|
40
40
|
* are gated off in `capabilities()` and rejected `unsupported_capability`. */
|
|
41
41
|
boundary?: () => PiBoundaryWiring | undefined;
|
|
42
|
+
/** The definition's names, as a LAZY thunk for the same reason {@link boundary} is one (the hub
|
|
43
|
+
* exists before the assembly that can read a definition) — and async because the definition is
|
|
44
|
+
* live: this must re-read it, not close over a boot snapshot, or `commands()` would advertise a
|
|
45
|
+
* list the next turn no longer runs.
|
|
46
|
+
*
|
|
47
|
+
* OPTIONAL because absence is a TRUE answer for the assembly that omits it: a hub over an L1
|
|
48
|
+
* agent (`createPiAgent({ model, instructions, tools })`) has no definition and therefore no
|
|
49
|
+
* names, and `[]` says exactly that. Wire it whenever the agent came from a DIRECTORY — there
|
|
50
|
+
* `[]` would be a lie; the directory constructor (`createPiAgentFromDir`) always does. */
|
|
51
|
+
commands?: () => Promise<AgentCommand[]>;
|
|
42
52
|
/** Tap for the events the HUB ITSELF generates (boundary mutations: `state_changed`,
|
|
43
53
|
* `compaction_*`) — those never pass through the data plane's observer seam, so a consumer
|
|
44
54
|
* composing a full-vocabulary tap wires the run events via the observer AND this. Called after
|
|
@@ -200,6 +200,9 @@ export function createPiSessionControl(options) {
|
|
|
200
200
|
fanOut(session, event);
|
|
201
201
|
};
|
|
202
202
|
const control = {
|
|
203
|
+
async commands() {
|
|
204
|
+
return (await options.commands?.()) ?? [];
|
|
205
|
+
},
|
|
203
206
|
capabilities: () => {
|
|
204
207
|
const b = boundary?.();
|
|
205
208
|
return {
|
package/dist/session-remote.js
CHANGED
|
@@ -65,6 +65,23 @@ export async function connectSessionControl(options) {
|
|
|
65
65
|
const capabilities = await get("/control/capabilities");
|
|
66
66
|
return {
|
|
67
67
|
capabilities: () => capabilities,
|
|
68
|
+
// NOT prefetched like capabilities: a live definition can grow a skill between calls, so the
|
|
69
|
+
// list is fetched per call. The endpoint is UNCACHED server-side (it re-reads the definition's
|
|
70
|
+
// skills/ per request), which is what keeps it honest about a directory that changes underneath
|
|
71
|
+
// it. A 404 is SKEW, not a fault in the definition: without this the two read identically
|
|
72
|
+
// (uncoded non-2xx), and a client would report "this agent's skills are unreadable" about a
|
|
73
|
+
// serve that simply predates the route.
|
|
74
|
+
async commands() {
|
|
75
|
+
try {
|
|
76
|
+
return await get("/control/commands");
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof ControlRequestError && error.status === 404) {
|
|
80
|
+
throw new ControlRequestError(404, "this serve does not implement /control/commands (it predates the route)");
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
68
85
|
state: (session) => get(`/control/state?session=${encodeURIComponent(session)}`),
|
|
69
86
|
entries: (session, opts) => get(`/control/entries?session=${encodeURIComponent(session)}${opts?.since !== undefined ? `&since=${encodeURIComponent(opts.since)}` : ""}`, PAYLOAD_TIMEOUT_MS),
|
|
70
87
|
async dispatch(session, command) {
|
package/dist/session.d.ts
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
import type { Json, Prompt } from "./agent.ts";
|
|
10
10
|
export interface SessionControl {
|
|
11
11
|
capabilities(): SessionCapabilities;
|
|
12
|
+
/** The names this agent exposes — what a composer's `/` completion LISTS. A listing, not a
|
|
13
|
+
* dispatch surface: the data plane takes prompts as text, so what typing one means (expanding it,
|
|
14
|
+
* sending "use the X skill", filtering a menu) is the client's business. Sessionless (the
|
|
15
|
+
* definition is a deployment fact) but ASYNC, because a definition is allowed to be live: an
|
|
16
|
+
* implementation that re-reads it per turn must answer from that same read, or the list and the
|
|
17
|
+
* behavior diverge. `[]` is a complete answer, not a missing one; a definition the implementation
|
|
18
|
+
* cannot read at all is a deployment fault and MAY reject. */
|
|
19
|
+
commands(): Promise<AgentCommand[]>;
|
|
12
20
|
state(session: string): Promise<SessionState>;
|
|
13
21
|
/** `since` is an APPEND-ORDER position cursor: "every record appended after the one with this
|
|
14
22
|
* id", regardless of branch structure. Reconstructing the active path in a branched session is
|
|
@@ -53,6 +61,18 @@ export interface SessionCapabilities {
|
|
|
53
61
|
toolProgress: boolean;
|
|
54
62
|
usage: boolean;
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* One name a client can offer the user. Field NAMES follow pi's RPC `get_commands` so a client
|
|
66
|
+
* porting from it maps directly; its `sourceInfo` (file provenance) is deliberately not carried, and
|
|
67
|
+
* `source` is a free-form string rather than pi's closed union — which kinds exist is an engine's
|
|
68
|
+
* business ("skill" is the only one fastagent assembles today), and an engine with none answers `[]`
|
|
69
|
+
* rather than the contract enumerating a set it cannot know.
|
|
70
|
+
*/
|
|
71
|
+
export interface AgentCommand {
|
|
72
|
+
name: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
source: string;
|
|
75
|
+
}
|
|
56
76
|
/** Stable `SessionResult.error.code` for a command the implementation does not support. */
|
|
57
77
|
export declare const UNSUPPORTED_CAPABILITY_CODE = "unsupported_capability";
|
|
58
78
|
/** Stable `SessionResult.error.code` for a run-modulating command (`steer`/`follow_up`/`abort`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on GitHub, Telegram, Slack, or behind any channel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|