@fastagent-sh/fastagent 0.16.1 → 0.16.2
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-state.js +7 -0
- package/dist/channels/control.js +3 -0
- package/dist/cli/commands/attach.d.ts +11 -1
- package/dist/cli/commands/attach.js +30 -2
- package/dist/cli/commands/deploy.js +11 -5
- package/dist/cli/commands/logs.d.ts +6 -0
- package/dist/cli/commands/logs.js +27 -0
- package/dist/cli/program.js +26 -0
- package/dist/deploy/agentcore/logs.d.ts +30 -0
- package/dist/deploy/agentcore/logs.js +115 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -0
- package/dist/deploy/agentcore/plan.js +40 -2
- package/dist/deploy/container.js +6 -3
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +6 -1
- package/dist/engines/pi/create.js +9 -3
- package/dist/engines/pi/harness.d.ts +12 -28
- package/dist/engines/pi/harness.js +21 -71
- package/dist/engines/pi/open.js +1 -0
- package/dist/engines/pi/session-control.d.ts +12 -3
- package/dist/engines/pi/session-control.js +156 -27
- package/dist/engines/pi/session-settings.d.ts +51 -0
- package/dist/engines/pi/session-settings.js +73 -0
- package/dist/engines/pi/sessions.d.ts +21 -7
- package/dist/engines/pi/sessions.js +43 -0
- package/dist/session.d.ts +34 -9
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
2
|
+
/** Which strings are levels at all — the vocabulary. What a MODEL supports is
|
|
3
|
+
* `getSupportedThinkingLevels`. The `satisfies` anchor keeps this exhaustive against pi's union: a
|
|
4
|
+
* level pi adds becomes a type error here rather than a value `set_thinking` silently rejects. */
|
|
5
|
+
const ALL_THINKING_LEVELS = {
|
|
6
|
+
off: true,
|
|
7
|
+
minimal: true,
|
|
8
|
+
low: true,
|
|
9
|
+
medium: true,
|
|
10
|
+
high: true,
|
|
11
|
+
xhigh: true,
|
|
12
|
+
max: true,
|
|
13
|
+
};
|
|
14
|
+
export const THINKING_LEVELS = new Set(Object.keys(ALL_THINKING_LEVELS));
|
|
15
|
+
/** The last entry of each kind wins, and a malformed one reads as ABSENT rather than falling through
|
|
16
|
+
* to an earlier record — which record is "the" override must not depend on who is asking. */
|
|
17
|
+
export function lastOverrideEntries(entries) {
|
|
18
|
+
let model;
|
|
19
|
+
let modelSeen = false;
|
|
20
|
+
let thinkingLevel;
|
|
21
|
+
let thinkingSeen = false;
|
|
22
|
+
for (let i = entries.length - 1; i >= 0 && !(modelSeen && thinkingSeen); i--) {
|
|
23
|
+
const e = entries[i];
|
|
24
|
+
if (!modelSeen && e?.type === "model_change") {
|
|
25
|
+
modelSeen = true;
|
|
26
|
+
if (e.provider !== undefined && e.modelId !== undefined)
|
|
27
|
+
model = { provider: e.provider, modelId: e.modelId };
|
|
28
|
+
}
|
|
29
|
+
if (!thinkingSeen && e?.type === "thinking_level_change") {
|
|
30
|
+
thinkingSeen = true;
|
|
31
|
+
if (e.thinkingLevel !== undefined)
|
|
32
|
+
thinkingLevel = e.thinkingLevel;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { model, thinkingLevel };
|
|
36
|
+
}
|
|
37
|
+
/** `defaults` is the assembly's configured pair — what a session with no overrides runs on. */
|
|
38
|
+
export function resolveSessionSettings(entries, models, defaults) {
|
|
39
|
+
const recorded = lastOverrideEntries(entries);
|
|
40
|
+
const dropped = {};
|
|
41
|
+
// A registry change across deploys must not brick the conversation.
|
|
42
|
+
let model = defaults.model;
|
|
43
|
+
if (recorded.model) {
|
|
44
|
+
const found = models.getModel(recorded.model.provider, recorded.model.modelId);
|
|
45
|
+
if (found)
|
|
46
|
+
model = found;
|
|
47
|
+
else
|
|
48
|
+
dropped.model = `${recorded.model.provider}/${recorded.model.modelId}`;
|
|
49
|
+
}
|
|
50
|
+
const availableThinkingLevels = getSupportedThinkingLevels(model);
|
|
51
|
+
let thinkingLevel = defaults.thinkingLevel;
|
|
52
|
+
if (recorded.thinkingLevel !== undefined) {
|
|
53
|
+
const level = recorded.thinkingLevel;
|
|
54
|
+
if (!THINKING_LEVELS.has(level)) {
|
|
55
|
+
dropped.thinkingLevel = { recorded: level, running: thinkingLevel, known: false };
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// pi's clamp takes the lowest supported level AT OR ABOVE the recorded one, falling back
|
|
59
|
+
// downward only when nothing is above: a gap resolves UPWARD and costs more, not less. Which is
|
|
60
|
+
// why `dropped` names both levels rather than reporting a substitution.
|
|
61
|
+
thinkingLevel = clampThinkingLevel(model, level);
|
|
62
|
+
if (thinkingLevel !== level) {
|
|
63
|
+
dropped.thinkingLevel = { recorded: level, running: thinkingLevel, known: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
model,
|
|
69
|
+
thinkingLevel,
|
|
70
|
+
availableThinkingLevels,
|
|
71
|
+
...(dropped.model || dropped.thinkingLevel ? { dropped } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Session } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import type { Session, SessionTreeEntry } from "@earendil-works/pi-agent-core";
|
|
2
2
|
/** What fastagent needs from a session backend: open-or-create by opaque id. */
|
|
3
3
|
export interface PiSessionStore {
|
|
4
4
|
openOrCreate(sessionId: string): Promise<Session>;
|
|
@@ -7,17 +7,31 @@ export interface PiSessionStore {
|
|
|
7
7
|
* OPEN-EXISTING sibling of {@link PiSessionStore} (session-control.ts): an unknown session answers
|
|
8
8
|
* `undefined`, never creates one — sessions are the data plane's monopoly. Two consumers:
|
|
9
9
|
* - the OBSERVATION plane (`state()`/`entries()`), strictly read-only (design §16 invariant 4);
|
|
10
|
-
* - the control plane's BOUNDARY writers (`set_model`/`set_thinking`
|
|
11
|
-
*
|
|
10
|
+
* - the control plane's BOUNDARY writers (`set_model`/`set_thinking` append override records to the
|
|
11
|
+
* returned handle; `navigate` moves its leaf) after an existence check, under the run lease.
|
|
12
12
|
* `openIfExists` skips the open-time crash reconciliation (that appends repair entries — a write
|
|
13
|
-
* the observation plane must not perform). The boundary writers are safe WITHOUT it
|
|
14
|
-
* override
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* the observation plane must not perform). The boundary writers are safe WITHOUT it for two
|
|
14
|
+
* different reasons: an override record is not a message, so it cannot create or pair with a
|
|
15
|
+
* dangling tool_use; a `navigate` writes no message either, but it CAN expose one — parking the
|
|
16
|
+
* leaf on an assistant entry whose tool results are now off-path is the dangling-pair state
|
|
17
|
+
* {@link reconcileInterruptedToolCalls} exists for. That is repaired at the next `openOrCreate`,
|
|
18
|
+
* which repairs AT THE LEAF — exactly where a move puts it.
|
|
19
|
+
*
|
|
20
|
+
* Writing MESSAGE-class records through this handle would bypass that repair: use `openOrCreate`
|
|
21
|
+
* for anything that enters the transcript.
|
|
17
22
|
*/
|
|
18
23
|
export interface PiSessionReader {
|
|
19
24
|
openIfExists(sessionId: string): Promise<Session | undefined>;
|
|
20
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The entries on the session's ACTIVE path, root→leaf — what every last-wins read must walk.
|
|
28
|
+
* `getEntries()` is the whole TREE: once `navigate` can move the leaf, the journal still carries
|
|
29
|
+
* the abandoned branch, and reading it flat would run the session on a setting it moved away from.
|
|
30
|
+
* Deliberately NOT `Session.getBranch()`: that walk is bounded by the last compaction's retained
|
|
31
|
+
* window, which is the right bound for MODEL CONTEXT and the wrong one for settings — an override
|
|
32
|
+
* recorded before a compaction is a preference, and it still governs the session after one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function activePathEntries(session: Session): Promise<SessionTreeEntry[]>;
|
|
21
35
|
/** In-process store (pi InMemorySessionRepo). Continuity lives and dies with the instance. */
|
|
22
36
|
export declare function inMemorySessionStore(): PiSessionStore & PiSessionReader;
|
|
23
37
|
/**
|
|
@@ -88,6 +88,49 @@ async function reconcileInterruptedToolCalls(session) {
|
|
|
88
88
|
await session.appendMessage(result);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The entries on the session's ACTIVE path, root→leaf — what every last-wins read must walk.
|
|
93
|
+
* `getEntries()` is the whole TREE: once `navigate` can move the leaf, the journal still carries
|
|
94
|
+
* the abandoned branch, and reading it flat would run the session on a setting it moved away from.
|
|
95
|
+
* Deliberately NOT `Session.getBranch()`: that walk is bounded by the last compaction's retained
|
|
96
|
+
* window, which is the right bound for MODEL CONTEXT and the wrong one for settings — an override
|
|
97
|
+
* recorded before a compaction is a preference, and it still governs the session after one.
|
|
98
|
+
*/
|
|
99
|
+
export async function activePathEntries(session) {
|
|
100
|
+
// LEAF FIRST, then the journal — the order is load-bearing, not incidental: `getEntries()` is a
|
|
101
|
+
// SNAPSHOT, so reading it first would let any concurrent append (i.e. any turn in progress) leave
|
|
102
|
+
// a leaf the snapshot cannot contain, and the integrity throw below would fire on a live session
|
|
103
|
+
// instead of a corrupt one. This way the snapshot is always a superset of the leaf's chain.
|
|
104
|
+
const leafId = await session.getLeafId();
|
|
105
|
+
const entries = await session.getEntries();
|
|
106
|
+
// A null leaf is pi's ROOT position (what `moveTo(null)` sets), so the active path is EMPTY — not
|
|
107
|
+
// "the whole journal", which on a branched session would mean every abandoned branch at once.
|
|
108
|
+
if (leafId === null)
|
|
109
|
+
return [];
|
|
110
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
111
|
+
// A chain that is not intact THROWS: any other disposition returns a short path that reads like a
|
|
112
|
+
// short session (every override and activation above the gap gone, the next turn silently on
|
|
113
|
+
// assembly defaults). pi's storages already reject a leaf outside the journal in `getLeafId()`,
|
|
114
|
+
// but the port is swappable — so the walk names it here rather than dying on `undefined.parentId`.
|
|
115
|
+
const start = byId.get(leafId);
|
|
116
|
+
if (!start)
|
|
117
|
+
throw new Error(`session leaf "${leafId}" is missing from the journal`);
|
|
118
|
+
const path = [];
|
|
119
|
+
for (let cur = start;;) {
|
|
120
|
+
path.push(cur);
|
|
121
|
+
// A path cannot be longer than the journal it walks; anything more is a parentId cycle, which
|
|
122
|
+
// would otherwise hang the turn with no `failed` event at all.
|
|
123
|
+
if (path.length > entries.length)
|
|
124
|
+
throw new Error(`session entry "${cur.id}" cycles through its parents`);
|
|
125
|
+
if (!cur.parentId)
|
|
126
|
+
break;
|
|
127
|
+
const parent = byId.get(cur.parentId);
|
|
128
|
+
if (!parent)
|
|
129
|
+
throw new Error(`session entry "${cur.parentId}" is missing from the journal (parent of "${cur.id}")`);
|
|
130
|
+
cur = parent;
|
|
131
|
+
}
|
|
132
|
+
return path.reverse(); // walked leaf→root; every consumer reads it root→leaf
|
|
133
|
+
}
|
|
91
134
|
/** In-process store (pi InMemorySessionRepo). Continuity lives and dies with the instance. */
|
|
92
135
|
export function inMemorySessionStore() {
|
|
93
136
|
const repo = new InMemorySessionRepo();
|
package/dist/session.d.ts
CHANGED
|
@@ -21,12 +21,17 @@ export interface SessionControl {
|
|
|
21
21
|
dispatch(session: string, command: SessionCommand): Promise<SessionResult>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* - COMMAND GATES (`steering`, `followUp`, `manualCompaction`, `modelSelection`, `thinkingLevel
|
|
24
|
+
* STATIC support declaration — sessionless, so nothing here may depend on a session. Two kinds of flag:
|
|
25
|
+
* - COMMAND GATES (`steering`, `followUp`, `manualCompaction`, `modelSelection`, `thinkingLevel`,
|
|
26
|
+
* `navigate`):
|
|
26
27
|
* clients MUST gate dispatch on them; an unsupported command is rejected before acceptance with
|
|
27
28
|
* {@link UNSUPPORTED_CAPABILITY_CODE}.
|
|
28
29
|
* - OBSERVATION-QUALITY flags (`toolProgress`, `usage`): whether those events/state fields appear
|
|
29
30
|
* at all — nothing to dispatch, nothing to reject.
|
|
31
|
+
*
|
|
32
|
+
* `modelSelection` may carry a list because the registry is a deployment fact; thinking levels
|
|
33
|
+
* depend on the session's current model, so they live on {@link SessionState.availableThinkingLevels}.
|
|
34
|
+
*
|
|
30
35
|
* `state`/`entries`/`events` are mandatory (the reconnect contract) and deliberately absent here.
|
|
31
36
|
*/
|
|
32
37
|
export interface SessionCapabilities {
|
|
@@ -36,9 +41,15 @@ export interface SessionCapabilities {
|
|
|
36
41
|
modelSelection: false | {
|
|
37
42
|
allowedModels: string[];
|
|
38
43
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
/** Whether `set_thinking` is servable at all. WHICH levels is per-session — see
|
|
45
|
+
* {@link SessionState.availableThinkingLevels}. */
|
|
46
|
+
thinkingLevel: boolean;
|
|
47
|
+
/** Whether `navigate` is servable at all — `false` both when the engine's sessions are linear and
|
|
48
|
+
* when this deployment has no write path for them; either way the tree the contract publishes
|
|
49
|
+
* (`SessionEntry.parentId` + `SessionEntries.leafEntryId`) is read-only here. Named after its COMMAND, unlike the older gates (`manualCompaction` gates
|
|
50
|
+
* `compact`, `thinkingLevel` gates `set_thinking`): those already force a client to translate,
|
|
51
|
+
* and a gate keyed by the command literal is the only naming a derived map could ever produce. */
|
|
52
|
+
navigate: boolean;
|
|
42
53
|
toolProgress: boolean;
|
|
43
54
|
usage: boolean;
|
|
44
55
|
}
|
|
@@ -79,7 +90,7 @@ export declare const NOTHING_TO_COMPACT_CODE = "nothing_to_compact";
|
|
|
79
90
|
* command as-is fails again. (A run registered without modulation controls is a capability
|
|
80
91
|
* problem, not a run problem, and rejects with {@link UNSUPPORTED_CAPABILITY_CODE}.) */
|
|
81
92
|
export declare const RUN_COMMAND_FAILED_CODE = "run_command_failed";
|
|
82
|
-
/**
|
|
93
|
+
/** Seven commands; deliberately NO `prompt` — starting work is the data plane's definition. */
|
|
83
94
|
export type SessionCommand = {
|
|
84
95
|
type: "steer";
|
|
85
96
|
prompt: Prompt;
|
|
@@ -97,6 +108,14 @@ export type SessionCommand = {
|
|
|
97
108
|
} | {
|
|
98
109
|
type: "set_thinking";
|
|
99
110
|
level: string;
|
|
111
|
+
}
|
|
112
|
+
/** Move the session's active leaf to `targetId`, an existing entry — the write verb for the tree
|
|
113
|
+
* `entries()` already publishes (and how sibling branches come to exist: the next turn hangs off
|
|
114
|
+
* the new leaf). Every entry `entries()` publishes is a legal target; a `targetId` that is not
|
|
115
|
+
* one rejects `invalid_command`. A boundary mutation otherwise: same lease as a run. */
|
|
116
|
+
| {
|
|
117
|
+
type: "navigate";
|
|
118
|
+
targetId: string;
|
|
100
119
|
};
|
|
101
120
|
/**
|
|
102
121
|
* Acceptance is not outcome: `ok: true` means admitted or applied, never that the run ultimately
|
|
@@ -125,10 +144,13 @@ export interface SessionState {
|
|
|
125
144
|
* compaction happens inside a run's activity window and reports as `running`. */
|
|
126
145
|
status: "idle" | "running" | "compacting";
|
|
127
146
|
activeRunId?: string;
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
147
|
+
/** What this session will RUN with, not what was recorded: overrides resolved against the
|
|
148
|
+
* deployment (a model the registry lost falls back to the configured one; a level the current
|
|
149
|
+
* model cannot do is clamped). Absent where the implementation exposes no model control. */
|
|
130
150
|
model?: string;
|
|
131
151
|
thinkingLevel?: string;
|
|
152
|
+
/** What `set_thinking` accepts for THIS session — re-read after a `set_model`. */
|
|
153
|
+
availableThinkingLevels?: string[];
|
|
132
154
|
pending: {
|
|
133
155
|
steering: number;
|
|
134
156
|
followUp: number;
|
|
@@ -222,10 +244,13 @@ export type QueueChangedEvent = SessionEvent<"queue_changed", {
|
|
|
222
244
|
runId: string;
|
|
223
245
|
};
|
|
224
246
|
/** A boundary mutation changed durable session state (L2; no runId — boundary mutations happen
|
|
225
|
-
* between runs).
|
|
247
|
+
* between runs). `leafEntryId` reports a `navigate` — a deliberate move of the branch head, which
|
|
248
|
+
* a second attached client would otherwise have no signal for. It is NOT a general leaf feed:
|
|
249
|
+
* every turn advances the leaf too, and that is read from `entries()`/`state()` after the run. */
|
|
226
250
|
export type StateChangedEvent = SessionEvent<"state_changed", {
|
|
227
251
|
model?: string;
|
|
228
252
|
thinkingLevel?: string;
|
|
253
|
+
leafEntryId?: string;
|
|
229
254
|
}>;
|
|
230
255
|
/** Manual compaction bounds (L2): every `compaction_started` is closed by exactly one
|
|
231
256
|
* `compaction_finished` — `summary` on success, `error` on failure, `aborted: true` on a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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",
|