-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnodeAgentChatAdapter.ts
More file actions
119 lines (106 loc) · 4.68 KB
/
Copy pathnodeAgentChatAdapter.ts
File metadata and controls
119 lines (106 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* NodeAgent <-> assistant-ui bridge.
*
* A `ChatModelAdapter` for assistant-ui's `useLocalRuntime`. When the user sends
* a message, this runs the real NodeAgent loop (gather -> search -> model ->
* memo) and streams it back as an assistant message whose work renders inline
* as four **tool UIs** — exactly the assistant-ui generative-UI pattern.
*
* No backend, no keys: the loop is deterministic over the demo scenario. Swap in
* a real model by replacing this adapter with `useChatRuntime` (AI SDK) or a
* fetch-backed adapter — the tool UIs and the modules stay identical.
*
* Prior art: assistant-ui LocalRuntime / ChatModelAdapter
* (https://github.com/assistant-ui/assistant-ui). See docs/ARCHITECTURE.md.
*/
import type { ChatModelAdapter } from "@assistant-ui/react";
import { runNodeAgent } from "./nodeAgentRuntime";
import { applySpreadsheetDelta } from "../../spreadsheet/applySpreadsheetDelta";
import { buildDemoScenario, DEMO_QUESTION } from "../demoScenario";
import type {
ContextBundle,
NotebookDoc,
SpreadsheetModel,
SynthesisResult,
AppliedDelta,
} from "../types/nodeAgentTypes";
/** Result payloads the four tool UIs receive (typed end to end). */
export type ContextToolResult = ContextBundle;
export type SearchToolResult = SynthesisResult;
export type ModelToolResult = { applied: AppliedDelta | null; model: SpreadsheetModel };
export type MemoToolResult = NotebookDoc;
const prefersReducedMotion = () =>
typeof window !== "undefined" &&
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
function lastUserText(messages: readonly { role: string; content: readonly unknown[] }[]): string {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.role !== "user") continue;
const text = (m.content as Array<{ type?: string; text?: string }>)
.filter((p) => p?.type === "text" && typeof p.text === "string")
.map((p) => p.text as string)
.join(" ")
.trim();
if (text) return text;
}
return "";
}
export const nodeAgentChatAdapter: ChatModelAdapter = {
async *run({ messages, abortSignal }) {
const question = lastUserText(messages) || DEMO_QUESTION;
// Run the real loop, then derive the post-delta model for the model tool UI.
const scenario = { ...buildDemoScenario(), question };
const result = runNodeAgent(scenario);
let model = scenario.model!;
if (scenario.modelDelta) {
const r = applySpreadsheetDelta(model, scenario.modelDelta, scenario.now);
if (r.ok) model = r.model;
}
const reduced = prefersReducedMotion();
const tick = (ms: number) =>
new Promise<void>((res) => setTimeout(res, reduced ? 0 : ms));
// Ordered map of tool-call parts; each yield replaces content cumulatively.
const parts = new Map<string, Record<string, unknown>>();
let lead =
"On it — pulling the room context, then searching, updating the model, and writing the memo.";
const snapshot = () => ({
content: [
{ type: "text" as const, text: lead },
...(Array.from(parts.values()) as never[]),
],
});
// Each step: show the tool running, then complete with its real result.
async function* step(
id: string,
toolName: string,
args: Record<string, unknown>,
toolResult: unknown,
) {
parts.set(id, { type: "tool-call", toolCallId: id, toolName, args });
yield snapshot();
if (abortSignal.aborted) return;
await tick(360);
parts.set(id, { type: "tool-call", toolCallId: id, toolName, args, result: toolResult });
yield snapshot();
await tick(160);
}
yield snapshot();
await tick(280);
yield* step("ctx", "collect_context", { focus: question }, result.context as ContextToolResult);
yield* step("search", "search_synthesize", { query: question }, result.synthesis as SearchToolResult);
yield* step(
"model",
"apply_spreadsheet_delta",
{ sheet: model.name },
{ applied: result.modelDelta, model } as ModelToolResult,
);
yield* step("memo", "write_memo", { title: result.memo.title }, result.memo as MemoToolResult);
// Closing line — honest about the run.
const runway = result.modelDelta?.changes.find((c) => c.address === "B3");
lead =
result.status === "ok"
? `Done. The wedge holds on ${result.synthesis.confidence}-confidence grounding (${result.synthesis.groundedCount}/${result.synthesis.sources.length}), the model now clears ${runway ? runway.to : "—"} months, and the cited memo is ready.`
: `Partial: I gathered context and updated the model, but grounding was too weak to synthesize a confident answer — flagged for manual review.`;
yield snapshot();
},
};