@cairnvibe/sdk 0.2.12 → 0.3.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/agent-loop.d.ts +113 -0
- package/dist/agent-loop.js +128 -0
- package/dist/cairn-widget.js +2 -2
- package/dist/element-ladder.d.ts +71 -0
- package/dist/element-ladder.js +168 -0
- package/dist/index.d.ts +79 -1
- package/dist/index.js +864 -89
- package/dist/key-rotator.d.ts +28 -0
- package/dist/key-rotator.js +57 -3
- package/dist/memory-sqlite.d.ts +86 -0
- package/dist/memory-sqlite.js +230 -0
- package/dist/realtime-cli.js +22 -1
- package/dist/realtime-server.d.ts +83 -2
- package/dist/realtime-server.js +549 -120
- package/dist/server.d.ts +249 -5
- package/dist/server.js +994 -71
- package/dist/skill-store.d.ts +17 -0
- package/dist/skill-store.js +78 -0
- package/dist/tts-stream.d.ts +25 -0
- package/dist/tts-stream.js +32 -0
- package/dist/vad.d.ts +27 -0
- package/dist/vad.js +128 -0
- package/dist/verb-executor.d.ts +32 -11
- package/dist/verb-executor.js +224 -16
- package/dist/webmcp-client.d.ts +14 -1
- package/dist/webmcp-client.js +22 -1
- package/package.json +3 -1
- package/src/agent-loop.ts +222 -0
- package/src/element-ladder.ts +170 -0
- package/src/index.tsx +914 -93
- package/src/key-rotator.ts +57 -2
- package/src/memory-sqlite.ts +283 -0
- package/src/realtime-cli.ts +24 -1
- package/src/realtime-server.ts +655 -122
- package/src/server.ts +1086 -71
- package/src/skill-store.ts +88 -0
- package/src/tts-stream.ts +30 -0
- package/src/vad.ts +153 -0
- package/src/verb-executor.ts +243 -22
- package/src/web-component.ts +82 -17
- package/src/webmcp-client.ts +30 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type AgentEvent, type CriticVerdict, type HistoryTurn, type VerbResponse } from "@cairnvibe/core";
|
|
2
|
+
/** 4 exchanges — matches the cap both original drivers independently used. */
|
|
3
|
+
export declare const MAX_HISTORY_TURNS = 8;
|
|
4
|
+
export declare function looksMultiStep(question: string): boolean;
|
|
5
|
+
export declare function summarizeVerbForHistory(verb: VerbResponse): string;
|
|
6
|
+
export interface AgentLoopStepEvent {
|
|
7
|
+
verb: VerbResponse;
|
|
8
|
+
/** 0-based. */
|
|
9
|
+
iteration: number;
|
|
10
|
+
/** True if isTerminalVerb(verb) says this ends the loop right after
|
|
11
|
+
* this hook returns (TERMINAL_VERBS membership, except a navigate
|
|
12
|
+
* marked continueAfter — see isTerminalVerb's own doc comment). Lets a
|
|
13
|
+
* caller act differently for a continuing vs. final step without
|
|
14
|
+
* re-deriving that check itself. */
|
|
15
|
+
terminal: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentLoopStepResultEvent {
|
|
18
|
+
verb: VerbResponse;
|
|
19
|
+
iteration: number;
|
|
20
|
+
observation: string | null | undefined;
|
|
21
|
+
}
|
|
22
|
+
export interface AgentLoopDeps {
|
|
23
|
+
/** Resolve the next step given the CURRENT working history. Return
|
|
24
|
+
* `null` for a response that failed to parse/validate — the HTTP
|
|
25
|
+
* path's own real case (a raw fetch response might not conform);
|
|
26
|
+
* realtime's in-process resolveVerb never produces this, since it
|
|
27
|
+
* always returns a valid VerbResponse itself. A null return ends the
|
|
28
|
+
* loop immediately with outcome "unparseable". */
|
|
29
|
+
getNextStep(loopHistory: HistoryTurn[], iteration: number): Promise<VerbResponse | null>;
|
|
30
|
+
/** Fires immediately after getNextStep resolves, before the terminal/
|
|
31
|
+
* continuing branch is decided — the real-time side-effect point (send
|
|
32
|
+
* the verb to a client, trigger an ack on the first continuing step,
|
|
33
|
+
* check for a superseding barge-in). Returning true aborts the loop
|
|
34
|
+
* immediately: no further side effects, outcome "aborted". */
|
|
35
|
+
onStep?(event: AgentLoopStepEvent): boolean | Promise<boolean>;
|
|
36
|
+
/** Execute a continuing verb (click/fill/read/call_tool/batch) for
|
|
37
|
+
* real; return its observation text (or null/undefined for "no
|
|
38
|
+
* result", folded into history as "no result" exactly like both
|
|
39
|
+
* original drivers already did). */
|
|
40
|
+
executeStep(verb: VerbResponse, iteration: number): Promise<string | null | undefined>;
|
|
41
|
+
/** Fires after executeStep resolves, before folding the observation
|
|
42
|
+
* into working history — a second real-time abort checkpoint (e.g. a
|
|
43
|
+
* barge-in generation check after awaiting a real tool result, which
|
|
44
|
+
* can itself take a while). Returning true aborts the loop with
|
|
45
|
+
* outcome "aborted", discarding this step's observation. */
|
|
46
|
+
onStepResult?(event: AgentLoopStepResultEvent): boolean | Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Phase 3 step 3 — a genuinely separate pass over the step's REAL
|
|
49
|
+
* observation, decoupled from the Executor/model's own self-report
|
|
50
|
+
* (the direct fix for the diagnosed bug: a batch succeeded and the
|
|
51
|
+
* model kept looping instead of recognizing it). Fires after
|
|
52
|
+
* onStepResult/the history fold. Returning a "task_complete" or
|
|
53
|
+
* "give_up" verdict ends the loop right here — even though the
|
|
54
|
+
* model's own verb was never a TERMINAL_VERBS member — instead of
|
|
55
|
+
* asking the model again and hoping it notices. Returning "continue"
|
|
56
|
+
* (including after the caller's own closure has silently handled a
|
|
57
|
+
* "replan" by fetching a fresh Plan — driveAgentLoop itself has no
|
|
58
|
+
* concept of a Plan, only of "keep going or stop") keeps the loop
|
|
59
|
+
* going exactly as if this hook were absent. Returning null/undefined
|
|
60
|
+
* behaves the same as "continue" — a caller can choose not to run the
|
|
61
|
+
* Critic on a particular step without a special no-op verdict shape.
|
|
62
|
+
*/
|
|
63
|
+
runCritic?(event: AgentLoopStepResultEvent): Promise<CriticVerdict | null | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* Phase 3 step 5 — a pure, fire-and-forget event consumer for a
|
|
66
|
+
* Talker-style narration layer ("Revisable by Design"'s pattern):
|
|
67
|
+
* never awaited, never able to affect control flow. driveAgentLoop
|
|
68
|
+
* itself emits "act" (right after a step's onStep/abort check passes —
|
|
69
|
+
* only for a verb that's actually going to execute, never a discarded
|
|
70
|
+
* one) and "obs" (right after onStepResult's own abort check passes),
|
|
71
|
+
* since it already has that data at exactly those points. A caller's
|
|
72
|
+
* own onStep/runCritic closures can call this SAME callback directly —
|
|
73
|
+
* it's just a plain reference they already have via the deps object
|
|
74
|
+
* they constructed — to emit "thk" (Critic reasoning) or "inj"
|
|
75
|
+
* (injected filler narration, e.g. a Talker ack phrase) events too;
|
|
76
|
+
* driveAgentLoop has no opinion on those.
|
|
77
|
+
*/
|
|
78
|
+
onEvent?(event: AgentEvent): void;
|
|
79
|
+
/** Defaults to 6 — a hard cap, not a target, matching both original drivers. */
|
|
80
|
+
maxIterations?: number;
|
|
81
|
+
}
|
|
82
|
+
export type AgentLoopOutcome = {
|
|
83
|
+
outcome: "terminal";
|
|
84
|
+
finalVerb: VerbResponse;
|
|
85
|
+
workingHistory: HistoryTurn[];
|
|
86
|
+
}
|
|
87
|
+
/** The Critic independently confirmed the (last) task's doneContract
|
|
88
|
+
* is satisfied — the real fix for the diagnosed bug. The caller
|
|
89
|
+
* synthesizes its own terminal-shaped response (e.g. `{verb: "explain",
|
|
90
|
+
* text: verdict.reasoning}`) from `verdict`, same as it would for a
|
|
91
|
+
* model-produced terminal verb. */
|
|
92
|
+
| {
|
|
93
|
+
outcome: "critic-complete";
|
|
94
|
+
verdict: CriticVerdict;
|
|
95
|
+
workingHistory: HistoryTurn[];
|
|
96
|
+
}
|
|
97
|
+
/** The Critic (or the harness's own stall-count fail-safe, inside the
|
|
98
|
+
* caller's runCritic closure) decided continuing wouldn't help. */
|
|
99
|
+
| {
|
|
100
|
+
outcome: "critic-give-up";
|
|
101
|
+
verdict: CriticVerdict;
|
|
102
|
+
workingHistory: HistoryTurn[];
|
|
103
|
+
} | {
|
|
104
|
+
outcome: "unparseable";
|
|
105
|
+
workingHistory: HistoryTurn[];
|
|
106
|
+
} | {
|
|
107
|
+
outcome: "gave-up";
|
|
108
|
+
workingHistory: HistoryTurn[];
|
|
109
|
+
} | {
|
|
110
|
+
outcome: "aborted";
|
|
111
|
+
workingHistory: HistoryTurn[];
|
|
112
|
+
};
|
|
113
|
+
export declare function driveAgentLoop(initialHistory: HistoryTurn[], deps: AgentLoopDeps): Promise<AgentLoopOutcome>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The shared skeleton behind both agent-loop drivers — index.tsx's
|
|
3
|
+
// runTypedAgentLoop (HTTP/typed transport) and realtime-server.ts's
|
|
4
|
+
// finalizeTurn (WebSocket/voice relay) independently re-implemented the
|
|
5
|
+
// exact same "ask, check terminal, execute a continuing step for real,
|
|
6
|
+
// fold the result into working history, ask again, up to a hard
|
|
7
|
+
// iteration cap" shape — a real, live duplication risk (any future fix
|
|
8
|
+
// to one had to be remembered and re-applied to the other by hand).
|
|
9
|
+
// This module is the first step of the Phase 3 multi-agent redesign
|
|
10
|
+
// (see DEVELOPMENT.md/the plan file's "Phase 3" entry): extract exactly
|
|
11
|
+
// that shared shape, with ZERO behavior change, so Planner/Critic
|
|
12
|
+
// wiring in later steps has one real place to attach to instead of two.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately does NOT own transport-specific side effects — sending a
|
|
15
|
+
// message to a client, speaking, committing to a connection's real
|
|
16
|
+
// cross-turn memory, barge-in cancellation timing. Those stay in each
|
|
17
|
+
// transport's own getNextStep/onStep/onStepResult/executeStep closures,
|
|
18
|
+
// and in what the caller does with this function's return value, exactly
|
|
19
|
+
// as before this extraction. Plain TypeScript only (no JSX, no Node
|
|
20
|
+
// built-ins) — imported as raw source by index.tsx's browser bundle AND
|
|
21
|
+
// compiled to dist/ for realtime-server.ts's Node build.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MAX_HISTORY_TURNS = void 0;
|
|
24
|
+
exports.looksMultiStep = looksMultiStep;
|
|
25
|
+
exports.summarizeVerbForHistory = summarizeVerbForHistory;
|
|
26
|
+
exports.driveAgentLoop = driveAgentLoop;
|
|
27
|
+
const core_1 = require("@cairnvibe/core");
|
|
28
|
+
/** 4 exchanges — matches the cap both original drivers independently used. */
|
|
29
|
+
exports.MAX_HISTORY_TURNS = 8;
|
|
30
|
+
/**
|
|
31
|
+
* Architecture Pillar 4 — a cheap, LOCAL signal for "this goal probably
|
|
32
|
+
* needs more than one real step," checked BEFORE the first step even
|
|
33
|
+
* runs, so a caller can start the Planner call in PARALLEL with the
|
|
34
|
+
* first getNextStep instead of only after that first step already came
|
|
35
|
+
* back non-terminal (the "lazy gate" the plan singles out for
|
|
36
|
+
* replacement — realtime-server.ts's own onStep used to build planPromise
|
|
37
|
+
* only once `!terminal && iteration === 0` was already true, one full
|
|
38
|
+
* model round trip later than it needed to be). Deliberately
|
|
39
|
+
* conservative, on purpose: a false negative here just falls back to
|
|
40
|
+
* that same lazy-after-step-1 behavior — unchanged, zero regression —
|
|
41
|
+
* while a false positive costs one Planner call that would have started
|
|
42
|
+
* a moment later anyway, never a wrong answer. Genuine UI-pattern-aware
|
|
43
|
+
* classification (Pillar 2, not built yet) can replace this heuristic
|
|
44
|
+
* later without changing what calls it. Lives here (not server.ts) so
|
|
45
|
+
* BOTH transports can use the exact same check: this file is plain,
|
|
46
|
+
* dependency-free TypeScript imported as raw source by index.tsx's
|
|
47
|
+
* browser bundle AND compiled for realtime-server.ts's Node build — a
|
|
48
|
+
* server-only file (server.ts imports the Anthropic/Groq SDKs) can never
|
|
49
|
+
* be imported from the client widget.
|
|
50
|
+
*/
|
|
51
|
+
const MULTI_STEP_SIGNAL = /\b(then|after that|once (you|it|that|i)|and then|next,|first[,.]? .*\bthen\b)\b/;
|
|
52
|
+
function looksMultiStep(question) {
|
|
53
|
+
return MULTI_STEP_SIGNAL.test(question.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
function summarizeVerbForHistory(verb) {
|
|
56
|
+
if ("text" in verb && verb.text)
|
|
57
|
+
return verb.text;
|
|
58
|
+
switch (verb.verb) {
|
|
59
|
+
case "highlight":
|
|
60
|
+
case "open":
|
|
61
|
+
return `(highlighted ${verb.target})`;
|
|
62
|
+
case "navigate":
|
|
63
|
+
return `(navigated to ${verb.route})`;
|
|
64
|
+
case "do":
|
|
65
|
+
return `(ran ${verb.action}${verb.target ? ` on ${verb.target}` : ""})`;
|
|
66
|
+
case "tour":
|
|
67
|
+
return verb.steps.map((s) => s.text).join(" ");
|
|
68
|
+
case "click":
|
|
69
|
+
return `(clicked ${verb.target})`;
|
|
70
|
+
case "fill":
|
|
71
|
+
return `(typed "${verb.value}" into ${verb.target})`;
|
|
72
|
+
case "read":
|
|
73
|
+
return `(read ${verb.target})`;
|
|
74
|
+
case "call_tool":
|
|
75
|
+
return `(called ${verb.name})`;
|
|
76
|
+
case "drag":
|
|
77
|
+
return `(dragged ${verb.target} to ${verb.to})`;
|
|
78
|
+
case "select":
|
|
79
|
+
return `(selected "${verb.value}" in ${verb.target})`;
|
|
80
|
+
case "key":
|
|
81
|
+
return `(pressed ${verb.key}${verb.target ? ` on ${verb.target}` : ""})`;
|
|
82
|
+
case "batch":
|
|
83
|
+
return `(${verb.actions.length} steps: ${verb.actions.map((a) => a.verb).join(", ")})`;
|
|
84
|
+
default:
|
|
85
|
+
return "(no response)";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function driveAgentLoop(initialHistory, deps) {
|
|
89
|
+
const maxIterations = deps.maxIterations ?? 6;
|
|
90
|
+
let loopHistory = initialHistory;
|
|
91
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
92
|
+
const verb = await deps.getNextStep(loopHistory, i);
|
|
93
|
+
if (verb === null)
|
|
94
|
+
return { outcome: "unparseable", workingHistory: loopHistory };
|
|
95
|
+
const terminal = (0, core_1.isTerminalVerb)(verb);
|
|
96
|
+
if (deps.onStep) {
|
|
97
|
+
const abort = await deps.onStep({ verb, iteration: i, terminal });
|
|
98
|
+
if (abort)
|
|
99
|
+
return { outcome: "aborted", workingHistory: loopHistory };
|
|
100
|
+
}
|
|
101
|
+
deps.onEvent?.({ type: "act", verb, at: Date.now() });
|
|
102
|
+
if (terminal) {
|
|
103
|
+
return { outcome: "terminal", finalVerb: verb, workingHistory: loopHistory };
|
|
104
|
+
}
|
|
105
|
+
const observation = await deps.executeStep(verb, i);
|
|
106
|
+
if (deps.onStepResult) {
|
|
107
|
+
const abort = await deps.onStepResult({ verb, iteration: i, observation });
|
|
108
|
+
if (abort)
|
|
109
|
+
return { outcome: "aborted", workingHistory: loopHistory };
|
|
110
|
+
}
|
|
111
|
+
deps.onEvent?.({ type: "obs", observation: observation ?? "no result", ok: observation !== null && observation !== undefined, at: Date.now() });
|
|
112
|
+
loopHistory = [
|
|
113
|
+
...loopHistory,
|
|
114
|
+
{ role: "assistant", text: `${summarizeVerbForHistory(verb)}. Result: ${observation ?? "no result"}` },
|
|
115
|
+
].slice(-exports.MAX_HISTORY_TURNS);
|
|
116
|
+
if (deps.runCritic) {
|
|
117
|
+
const verdict = await deps.runCritic({ verb, iteration: i, observation });
|
|
118
|
+
if (verdict?.verdict === "task_complete")
|
|
119
|
+
return { outcome: "critic-complete", verdict, workingHistory: loopHistory };
|
|
120
|
+
if (verdict?.verdict === "give_up")
|
|
121
|
+
return { outcome: "critic-give-up", verdict, workingHistory: loopHistory };
|
|
122
|
+
// "continue", "replan" (already handled inside the caller's own
|
|
123
|
+
// runCritic closure — see this field's own doc comment), or no
|
|
124
|
+
// verdict at all: fall through and keep looping, unchanged.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { outcome: "gave-up", workingHistory: loopHistory };
|
|
128
|
+
}
|