logisheets-logician 1.11.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.
Files changed (49) hide show
  1. package/README.md +89 -0
  2. package/dist/agent/loop.d.ts +101 -0
  3. package/dist/agent/loop.js +260 -0
  4. package/dist/conversation.d.ts +105 -0
  5. package/dist/conversation.js +10 -0
  6. package/dist/craft-interactions-api.d.ts +81 -0
  7. package/dist/craft-interactions-api.js +25 -0
  8. package/dist/craft-interactions-core.d.ts +14 -0
  9. package/dist/craft-interactions-core.js +33 -0
  10. package/dist/crafts/manifest.d.ts +39 -0
  11. package/dist/crafts/manifest.js +10 -0
  12. package/dist/crafts/skill-tools.d.ts +33 -0
  13. package/dist/crafts/skill-tools.js +146 -0
  14. package/dist/crafts/store.d.ts +47 -0
  15. package/dist/crafts/store.js +69 -0
  16. package/dist/index.d.ts +22 -0
  17. package/dist/index.js +22 -0
  18. package/dist/index.node.js +5065 -0
  19. package/dist/projection.d.ts +129 -0
  20. package/dist/projection.js +249 -0
  21. package/dist/storage.d.ts +99 -0
  22. package/dist/storage.js +223 -0
  23. package/dist/tool.d.ts +137 -0
  24. package/dist/tool.js +57 -0
  25. package/dist/tools/block-ops.d.ts +30 -0
  26. package/dist/tools/block-ops.js +97 -0
  27. package/dist/tools/builder.d.ts +206 -0
  28. package/dist/tools/builder.js +1502 -0
  29. package/dist/tools/cells.d.ts +58 -0
  30. package/dist/tools/cells.js +263 -0
  31. package/dist/tools/comments.d.ts +54 -0
  32. package/dist/tools/comments.js +234 -0
  33. package/dist/tools/craft-interactions.d.ts +22 -0
  34. package/dist/tools/craft-interactions.js +454 -0
  35. package/dist/tools/edit.d.ts +55 -0
  36. package/dist/tools/edit.js +356 -0
  37. package/dist/tools/format.d.ts +58 -0
  38. package/dist/tools/format.js +208 -0
  39. package/dist/tools/history.d.ts +13 -0
  40. package/dist/tools/history.js +39 -0
  41. package/dist/tools/inspect.d.ts +93 -0
  42. package/dist/tools/inspect.js +488 -0
  43. package/dist/tools/links.d.ts +30 -0
  44. package/dist/tools/links.js +122 -0
  45. package/dist/tools/structure.d.ts +62 -0
  46. package/dist/tools/structure.js +174 -0
  47. package/dist/tools/taxonomy.d.ts +32 -0
  48. package/dist/tools/taxonomy.js +93 -0
  49. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # logician
2
+
3
+ The **AI agent core for [LogiSheets](https://github.com/logisky/LogiSheets)** — a
4
+ platform-agnostic toolkit of LLM tools, prompts, and an agent loop for operating
5
+ spreadsheets through the LogiSheets engine. It powers **Watson**, the in-app AI
6
+ assistant, and is reusable from any host: the browser, a Node CLI, or an MCP
7
+ server.
8
+
9
+ `logician` is engine-neutral: it drives a workbook only through the
10
+ `WorkbookClient` surface from
11
+ [`logisheets-core`](https://www.npmjs.com/package/logisheets-core) /
12
+ [`logisheets-web`](https://www.npmjs.com/package/logisheets-web), so the same
13
+ agent runs in the browser and headless on Node.
14
+
15
+ > **Status:** internal to the LogiSheets monorepo (`private`), evolving. The API
16
+ > below is stable enough to build on but not yet semver-guaranteed.
17
+
18
+ ## What's inside
19
+
20
+ - **Tool groups** — ready-made LLM tools grouped by capability, each a set of
21
+ typed `Tool`s with JSON-schema inputs:
22
+ - `INSPECT_TOOLS` — read the workbook (list blocks, describe a block, read a
23
+ selection, evaluate a formula, explain why a cell is locked, …).
24
+ - `EDIT_TOOLS` — mutate cells and blocks (set cells, add/delete block rows,
25
+ checkpoint, preview changes, …).
26
+ - `BUILDER_TOOLS` — higher-level authoring (create sheets and blocks, define
27
+ field rules and enum sets, …).
28
+ - `CRAFT_INTERACTION_TOOLS` — reach the host's craft overlay/widget state.
29
+ - **`ToolRegistry`** — register tools and emit them as Anthropic tool
30
+ definitions (`toLlmTools()`).
31
+ - **`Agent`** — an end-to-end turn loop (`runTurn`) that drives an LLM ↔ tools
32
+ cycle until the model finishes, with per-tool confirmation policies
33
+ (`once` / `always` / `destructive`) for safe, human-in-the-loop editing.
34
+ - **Conversation store** — an event-sourced conversation model with adapters for
35
+ Anthropic messages (`toLlmMessages`) and host UIs (`toUiBubbles`), plus an
36
+ in-memory implementation.
37
+
38
+ ## Usage
39
+
40
+ ```ts
41
+ import {
42
+ Agent,
43
+ ToolRegistry,
44
+ INSPECT_TOOLS,
45
+ EDIT_TOOLS,
46
+ BUILDER_TOOLS,
47
+ MemoryConversationStore,
48
+ } from 'logisheets-logician'
49
+ import type {WorkbookClient} from 'logisheets-web'
50
+
51
+ declare const workbook: WorkbookClient // from the host (browser worker or Node)
52
+ declare const llm: LlmClient // your Anthropic client adapter
53
+
54
+ const registry = new ToolRegistry()
55
+ registry.registerMany([...INSPECT_TOOLS, ...EDIT_TOOLS, ...BUILDER_TOOLS])
56
+
57
+ const agent = new Agent({
58
+ store: new MemoryConversationStore(),
59
+ registry,
60
+ llm,
61
+ workbook,
62
+ systemPrompt: 'You operate a LogiSheets workbook on the user\'s behalf.',
63
+ })
64
+
65
+ await agent.runTurn(conversationId, 'Add a Revenue column and total it.')
66
+ ```
67
+
68
+ The `confirm` callback lets the host approve destructive or policy-gated tool
69
+ calls (the browser wires it to a modal; a CLI can prompt on stdin; headless hosts
70
+ auto-approve).
71
+
72
+ ## Where it fits
73
+
74
+ ```
75
+ logisheets-web / logisheets (WASM engine)
76
+ │ WorkbookClient
77
+
78
+ logisheets-core
79
+
80
+
81
+ logician ← LLM tools + agent loop
82
+
83
+ ┌────────┴─────────┐
84
+ Watson (browser) Node CLI / MCP server
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT — part of the [LogiSheets](https://github.com/logisky/LogiSheets) project.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Agent loop — drive a multi-turn conversation:
3
+ * user_message → LLM → (text + tool_use*) → execute tools → repeat
4
+ * until stop_reason === 'end_turn'.
5
+ *
6
+ * The loop is platform-agnostic. It depends on:
7
+ * - ConversationStore (events + blobs)
8
+ * - ToolRegistry (handler dispatch)
9
+ * - LlmClient (Anthropic, or any drop-in)
10
+ * - WorkbookClient (passed through to tool handlers)
11
+ * - host-provided confirm / log callbacks
12
+ *
13
+ * The Anthropic SDK is NOT imported here. Hosts wire up a concrete
14
+ * LlmClient (the browser craft uses fetch + the Messages REST API, the
15
+ * node CLI uses @anthropic-ai/sdk). Keeping the boundary thin lets us
16
+ * test the loop without network and swap providers later.
17
+ */
18
+ import type { ConversationEvent } from '../conversation.js';
19
+ import { type AgentContentBlock, type AgentMessage } from '../projection.js';
20
+ import type { ConversationStore } from '../storage.js';
21
+ import { toLlmTool, ToolRegistry } from '../tool.js';
22
+ import type { WorkbookClient } from '../tool.js';
23
+ import type { CraftInteractionsApi } from '../craft-interactions-api.js';
24
+ export interface LlmCreateMessageParams {
25
+ model: string;
26
+ system: AgentSystemBlock[];
27
+ tools: ReturnType<typeof toLlmTool>[];
28
+ messages: AgentMessage[];
29
+ max_tokens: number;
30
+ signal?: AbortSignal;
31
+ }
32
+ export interface AgentSystemBlock {
33
+ type: 'text';
34
+ text: string;
35
+ /** Cache breakpoint marker. */
36
+ cache_control?: {
37
+ type: 'ephemeral';
38
+ };
39
+ }
40
+ export interface LlmResponse {
41
+ /** Anthropic-style response content. */
42
+ content: AgentContentBlock[];
43
+ stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | string;
44
+ usage?: {
45
+ input_tokens: number;
46
+ output_tokens: number;
47
+ cache_creation_input_tokens?: number;
48
+ cache_read_input_tokens?: number;
49
+ };
50
+ }
51
+ export interface LlmClient {
52
+ createMessage(params: LlmCreateMessageParams): Promise<LlmResponse>;
53
+ }
54
+ export interface AgentOptions {
55
+ store: ConversationStore;
56
+ registry: ToolRegistry;
57
+ llm: LlmClient;
58
+ workbook: WorkbookClient;
59
+ /** Anthropic model id. Default 'claude-opus-4-8'. */
60
+ model?: string;
61
+ /** Cap on tokens per response. Default 4096. */
62
+ max_tokens?: number;
63
+ /** Base system prompt (instructions). Tool listing is auto-appended. */
64
+ systemPrompt: string;
65
+ /** Defensive cap on tool calls per user turn to prevent runaway loops. */
66
+ max_tool_iterations?: number;
67
+ /** Confirmation prompt for tools whose policy demands it. */
68
+ confirm?: (toolName: string, input: unknown, policy: 'once' | 'always' | 'destructive') => Promise<{
69
+ approved: boolean;
70
+ reason?: string;
71
+ }>;
72
+ /** Hook for surfacing in-flight notes to the host UI (toast / log line). */
73
+ log?: (msg: string) => void;
74
+ /** Optional craft-interaction capability surface; passed to every
75
+ * ToolContext so craft-interaction tools can reach the host's
76
+ * overlay-widget state. Omit on headless hosts. */
77
+ craftInteractions?: CraftInteractionsApi;
78
+ }
79
+ export declare class Agent {
80
+ private store;
81
+ private registry;
82
+ private llm;
83
+ private workbook;
84
+ private model;
85
+ private maxTokens;
86
+ private systemPrompt;
87
+ private maxToolIters;
88
+ private confirm;
89
+ private log;
90
+ private craftInteractions?;
91
+ constructor(opts: AgentOptions);
92
+ /**
93
+ * Run one user turn end-to-end: append the user_message, then loop
94
+ * LLM ↔ tools until the model emits end_turn or we hit a safety cap.
95
+ */
96
+ runTurn(conversation_id: string, userText: string, signal?: AbortSignal): Promise<void>;
97
+ private executeToolCall;
98
+ private buildSystem;
99
+ private appendNote;
100
+ }
101
+ export type { ConversationEvent };
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Agent loop — drive a multi-turn conversation:
3
+ * user_message → LLM → (text + tool_use*) → execute tools → repeat
4
+ * until stop_reason === 'end_turn'.
5
+ *
6
+ * The loop is platform-agnostic. It depends on:
7
+ * - ConversationStore (events + blobs)
8
+ * - ToolRegistry (handler dispatch)
9
+ * - LlmClient (Anthropic, or any drop-in)
10
+ * - WorkbookClient (passed through to tool handlers)
11
+ * - host-provided confirm / log callbacks
12
+ *
13
+ * The Anthropic SDK is NOT imported here. Hosts wire up a concrete
14
+ * LlmClient (the browser craft uses fetch + the Messages REST API, the
15
+ * node CLI uses @anthropic-ai/sdk). Keeping the boundary thin lets us
16
+ * test the loop without network and swap providers later.
17
+ */
18
+ import { toLlmMessages, } from '../projection.js';
19
+ export class Agent {
20
+ constructor(opts) {
21
+ var _a, _b, _c, _d, _e;
22
+ this.store = opts.store;
23
+ this.registry = opts.registry;
24
+ this.llm = opts.llm;
25
+ this.workbook = opts.workbook;
26
+ this.model = (_a = opts.model) !== null && _a !== void 0 ? _a : 'claude-opus-4-8';
27
+ this.maxTokens = (_b = opts.max_tokens) !== null && _b !== void 0 ? _b : 4096;
28
+ this.systemPrompt = opts.systemPrompt;
29
+ this.maxToolIters = (_c = opts.max_tool_iterations) !== null && _c !== void 0 ? _c : 16;
30
+ // Default confirm: auto-approve. Browser host overrides with a
31
+ // real modal. CLI host can override with stdin prompt.
32
+ this.confirm =
33
+ (_d = opts.confirm) !== null && _d !== void 0 ? _d : (async () => ({ approved: true }));
34
+ this.log = (_e = opts.log) !== null && _e !== void 0 ? _e : (() => { });
35
+ this.craftInteractions = opts.craftInteractions;
36
+ }
37
+ /**
38
+ * Run one user turn end-to-end: append the user_message, then loop
39
+ * LLM ↔ tools until the model emits end_turn or we hit a safety cap.
40
+ */
41
+ async runTurn(conversation_id, userText, signal) {
42
+ const userEvent = {
43
+ kind: 'user_message',
44
+ id: newEventId(),
45
+ conversation_id,
46
+ ts: nowMs(),
47
+ text: userText,
48
+ };
49
+ await this.store.appendEvent(userEvent);
50
+ const blobResolver = (ref) => {
51
+ // listEvents is async; the loop pre-hydrates blobs into a
52
+ // synchronous cache before each LLM call below. Default
53
+ // fallback returns null (projection emits a placeholder).
54
+ return null;
55
+ };
56
+ let iter = 0;
57
+ for (;;) {
58
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
59
+ await this.appendNote(conversation_id, 'Turn aborted by user.', 'ui_only');
60
+ return;
61
+ }
62
+ if (iter++ > this.maxToolIters) {
63
+ await this.appendNote(conversation_id, `Tool-iteration cap (${this.maxToolIters}) reached. Stopping.`, 'ui_only');
64
+ return;
65
+ }
66
+ const events = await this.store.listEvents(conversation_id);
67
+ const messages = toLlmMessages(events, { resolveBlob: blobResolver });
68
+ const response = await this.llm.createMessage({
69
+ model: this.model,
70
+ system: this.buildSystem(),
71
+ tools: this.registry.toLlmTools(),
72
+ messages,
73
+ max_tokens: this.maxTokens,
74
+ signal,
75
+ });
76
+ const turn_id = newTurnId();
77
+ const toolCalls = [];
78
+ // First pass: persist every assistant content block as an event
79
+ // *before* executing any tool. This way a crash mid-tool-run
80
+ // leaves the assistant's intent on record and we can recover.
81
+ for (const block of response.content) {
82
+ if (block.type === 'text') {
83
+ const e = {
84
+ kind: 'assistant_text',
85
+ id: newEventId(),
86
+ conversation_id,
87
+ ts: nowMs(),
88
+ text: block.text,
89
+ turn_id,
90
+ };
91
+ await this.store.appendEvent(e);
92
+ }
93
+ else if (block.type === 'tool_use') {
94
+ const e = {
95
+ kind: 'tool_call',
96
+ id: newEventId(),
97
+ conversation_id,
98
+ ts: nowMs(),
99
+ tool_use_id: block.id,
100
+ name: block.name,
101
+ input: block.input,
102
+ turn_id,
103
+ };
104
+ await this.store.appendEvent(e);
105
+ toolCalls.push({
106
+ id: block.id,
107
+ name: block.name,
108
+ input: block.input,
109
+ });
110
+ }
111
+ // tool_result blocks in the response don't happen — the
112
+ // model never produces them, only consumes them.
113
+ }
114
+ // Second pass: execute every tool the model asked for. Anthropic
115
+ // requires *all* tool_results for the previous turn before the
116
+ // next request, so we walk them sequentially here.
117
+ for (const call of toolCalls) {
118
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
119
+ return;
120
+ await this.executeToolCall(conversation_id, call, signal);
121
+ }
122
+ if (response.stop_reason === 'end_turn')
123
+ break;
124
+ if (response.stop_reason === 'max_tokens') {
125
+ await this.appendNote(conversation_id, 'Response truncated at max_tokens.', 'ui_only');
126
+ break;
127
+ }
128
+ // Any other stop_reason (incl. 'tool_use') — loop to next request.
129
+ }
130
+ }
131
+ // -----------------------------------------------------------------------
132
+ async executeToolCall(conversation_id, call, signal) {
133
+ var _a, _b;
134
+ const tool = this.registry.get(call.name);
135
+ const start = nowMs();
136
+ if (!tool) {
137
+ const e = {
138
+ kind: 'tool_result',
139
+ id: newEventId(),
140
+ conversation_id,
141
+ ts: nowMs(),
142
+ tool_use_id: call.id,
143
+ output: null,
144
+ error: `Unknown tool: ${call.name}`,
145
+ duration_ms: 0,
146
+ };
147
+ await this.store.appendEvent(e);
148
+ return;
149
+ }
150
+ // Confirmation gate.
151
+ const policy = (_a = tool.confirmation) !== null && _a !== void 0 ? _a : (tool.mutates ? 'always' : 'never');
152
+ if (policy !== 'never') {
153
+ const decision = await this.confirm(call.name, call.input, policy);
154
+ const ev = {
155
+ kind: 'user_confirm',
156
+ id: newEventId(),
157
+ conversation_id,
158
+ ts: nowMs(),
159
+ tool_use_id: call.id,
160
+ approved: decision.approved,
161
+ reason: decision.reason,
162
+ };
163
+ await this.store.appendEvent(ev);
164
+ if (!decision.approved) {
165
+ const e = {
166
+ kind: 'tool_result',
167
+ id: newEventId(),
168
+ conversation_id,
169
+ ts: nowMs(),
170
+ tool_use_id: call.id,
171
+ output: null,
172
+ error: `User declined${decision.reason ? `: ${decision.reason}` : '.'}`,
173
+ duration_ms: nowMs() - start,
174
+ };
175
+ await this.store.appendEvent(e);
176
+ return;
177
+ }
178
+ }
179
+ // Run handler.
180
+ const ctx = {
181
+ workbook: this.workbook,
182
+ signal: signal !== null && signal !== void 0 ? signal : new AbortController().signal,
183
+ confirm: async (msg, detail) => {
184
+ const d = await this.confirm(call.name, { message: msg, detail }, 'always');
185
+ return d.approved;
186
+ },
187
+ log: (msg) => this.log(`[${call.name}] ${msg}`),
188
+ craftInteractions: this.craftInteractions,
189
+ };
190
+ try {
191
+ const result = await tool.handler(call.input, ctx);
192
+ const e = {
193
+ kind: 'tool_result',
194
+ id: newEventId(),
195
+ conversation_id,
196
+ ts: nowMs(),
197
+ tool_use_id: call.id,
198
+ output: result.canceled
199
+ ? { canceled: true }
200
+ : (_b = result.data) !== null && _b !== void 0 ? _b : null,
201
+ duration_ms: nowMs() - start,
202
+ };
203
+ await this.store.appendEvent(e);
204
+ }
205
+ catch (err) {
206
+ const e = {
207
+ kind: 'tool_result',
208
+ id: newEventId(),
209
+ conversation_id,
210
+ ts: nowMs(),
211
+ tool_use_id: call.id,
212
+ output: null,
213
+ error: err instanceof Error ? err.message : String(err),
214
+ duration_ms: nowMs() - start,
215
+ };
216
+ await this.store.appendEvent(e);
217
+ }
218
+ }
219
+ buildSystem() {
220
+ // Two blocks so prompt caching works cleanly:
221
+ // [0] = user-supplied system prompt (stable, cached)
222
+ // [1] = tool list (also stable across a turn; we cache here too
223
+ // because the same tools are reused across many turns)
224
+ // The Messages API treats every block as a sub-prompt; the last
225
+ // cache_control marker wins for that prefix.
226
+ return [
227
+ {
228
+ type: 'text',
229
+ text: this.systemPrompt,
230
+ cache_control: { type: 'ephemeral' },
231
+ },
232
+ ];
233
+ }
234
+ async appendNote(conversation_id, text, visibility) {
235
+ const e = {
236
+ kind: 'system_note',
237
+ id: newEventId(),
238
+ conversation_id,
239
+ ts: nowMs(),
240
+ text,
241
+ visibility,
242
+ };
243
+ await this.store.appendEvent(e);
244
+ }
245
+ }
246
+ // ---------------------------------------------------------------------------
247
+ // id / time helpers — kept local so the loop has no implicit globals
248
+ // ---------------------------------------------------------------------------
249
+ function newEventId() {
250
+ return `evt_${nowMs().toString(36)}_${shortRand()}`;
251
+ }
252
+ function newTurnId() {
253
+ return `turn_${nowMs().toString(36)}_${shortRand()}`;
254
+ }
255
+ function nowMs() {
256
+ return Date.now();
257
+ }
258
+ function shortRand() {
259
+ return Math.random().toString(36).slice(2, 10);
260
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Conversation event stream — the single source of truth for an
3
+ * agent session. UI transcripts and LLM `messages` arrays are both
4
+ * pure projections of this stream.
5
+ *
6
+ * Events are append-only. The agent loop never rewrites or deletes.
7
+ * Old events may be elided via summarization (a `system_note` replacing
8
+ * a prefix) but the original events remain for replay and debugging.
9
+ */
10
+ /** Discriminated union of everything that can happen in a conversation. */
11
+ export type ConversationEvent = UserMessageEvent | AssistantTextEvent | ToolCallEvent | ToolResultEvent | UserConfirmEvent | SystemNoteEvent;
12
+ export interface BaseEvent {
13
+ /** Unique id, monotonically sortable (UUIDv7 or `${ts}-${rand}`). */
14
+ id: string;
15
+ /** Conversation foreign key. */
16
+ conversation_id: string;
17
+ /** Epoch ms when the event was recorded. */
18
+ ts: number;
19
+ }
20
+ export interface UserMessageEvent extends BaseEvent {
21
+ kind: 'user_message';
22
+ text: string;
23
+ }
24
+ export interface AssistantTextEvent extends BaseEvent {
25
+ kind: 'assistant_text';
26
+ text: string;
27
+ /**
28
+ * The model can emit multiple text blocks interleaved with tool calls
29
+ * in a single turn. `turn_id` groups events that belong to the same
30
+ * LLM response.
31
+ */
32
+ turn_id: string;
33
+ }
34
+ export interface ToolCallEvent extends BaseEvent {
35
+ kind: 'tool_call';
36
+ /** Anthropic's tool_use_id — used to correlate with tool_result. */
37
+ tool_use_id: string;
38
+ /** Fully-qualified tool name, e.g. "build__create_block". */
39
+ name: string;
40
+ /** Validated input object. */
41
+ input: unknown;
42
+ turn_id: string;
43
+ }
44
+ export interface ToolResultEvent extends BaseEvent {
45
+ kind: 'tool_result';
46
+ tool_use_id: string;
47
+ /**
48
+ * Output payload, or a blob ref when the value is large. Large outputs
49
+ * (e.g. describe_block with include_rows=true) should be stored via
50
+ * `ConversationStore.putBlob` and referenced here as
51
+ * `{kind: 'blob_ref', ref: '...'}` to keep events table compact.
52
+ */
53
+ output: unknown;
54
+ /** Set when the handler threw. `output` then holds an error summary. */
55
+ error?: string;
56
+ /** Total time the handler spent, ms. */
57
+ duration_ms: number;
58
+ }
59
+ export interface UserConfirmEvent extends BaseEvent {
60
+ kind: 'user_confirm';
61
+ tool_use_id: string;
62
+ approved: boolean;
63
+ /** Optional reason the user typed when rejecting. */
64
+ reason?: string;
65
+ }
66
+ export interface SystemNoteEvent extends BaseEvent {
67
+ kind: 'system_note';
68
+ text: string;
69
+ /**
70
+ * Tag used by projection logic to decide visibility. e.g.:
71
+ * 'summary' — replaces an elided prefix, must enter LLM context
72
+ * 'ui_only' — surface to user but skip in LLM messages
73
+ * 'debug' — internal trace, neither projected to UI nor LLM
74
+ */
75
+ visibility: 'summary' | 'ui_only' | 'debug';
76
+ }
77
+ export interface Conversation {
78
+ id: string;
79
+ /** Auto-generated or user-renamed, shown in the sidebar. */
80
+ title: string;
81
+ /** Workbook this conversation is bound to. Optional for cross-workbook chats. */
82
+ workbook_id?: string;
83
+ /** Anthropic model id, e.g. "claude-opus-4-8". */
84
+ model: string;
85
+ created_at: number;
86
+ updated_at: number;
87
+ /** Optional free-form metadata for forward-compat (skill name, locale, …). */
88
+ extra?: Record<string, unknown>;
89
+ }
90
+ /** Lightweight shape for sidebars / pickers — no events. */
91
+ export interface ConversationSummary {
92
+ id: string;
93
+ title: string;
94
+ workbook_id?: string;
95
+ updated_at: number;
96
+ /** Cached event count for the badge; null means "unknown, query if needed". */
97
+ event_count?: number;
98
+ }
99
+ /** Bundle returned by an export. */
100
+ export interface ConversationExport {
101
+ conversation: Conversation;
102
+ events: ConversationEvent[];
103
+ /** Inlined blob payloads keyed by ref. */
104
+ blobs: Record<string, string | Uint8Array>;
105
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Conversation event stream — the single source of truth for an
3
+ * agent session. UI transcripts and LLM `messages` arrays are both
4
+ * pure projections of this stream.
5
+ *
6
+ * Events are append-only. The agent loop never rewrites or deletes.
7
+ * Old events may be elided via summarization (a `system_note` replacing
8
+ * a prefix) but the original events remain for replay and debugging.
9
+ */
10
+ export {};
@@ -0,0 +1,81 @@
1
+ /**
2
+ * CraftInteractionsApi — the host capability surface for craft-defined
3
+ * cell-overlay widgets (radio groups, multi-select, point allocators,
4
+ * percent allocators, number sliders).
5
+ *
6
+ * Scope: BUILD + READ. This mirrors what the host injects into a craft
7
+ * iframe (`injectCraftInteractionAPIs`): a craft *registers* overlays onto
8
+ * block cells and *reads* what the user selected — it does NOT
9
+ * programmatically set selections (the user operates the overlay by
10
+ * clicking). Watson, being a craft that builds factory-simulator-like
11
+ * apps, needs exactly this build+read surface.
12
+ *
13
+ * Why an interface in logician (not an import of the host module):
14
+ * craft-interaction state is module singletons in the browser host
15
+ * (`src/core/craft-interactions`), which logician must not depend on —
16
+ * logician is platform-agnostic and reusable from a Node CLI / MCP server.
17
+ * The host implements this interface and passes it via
18
+ * `ToolContext.craftInteractions`; tools program against the interface
19
+ * only and report "not available" when it's absent.
20
+ *
21
+ * Method names and binding shapes mirror the injected window API 1:1, so
22
+ * the Watson host adapter is a thin pass-through:
23
+ * { registerRadio: window.registerRadio, ... }
24
+ */
25
+ /** A cell an overlay binds to. `row`/`col` are block-relative offsets —
26
+ * the same coordinates the host singletons use. Tools resolve these from
27
+ * (block ref, field, row_key) via the block schema before calling. */
28
+ export interface InteractionCell {
29
+ groupId: string;
30
+ sheetIdx: number;
31
+ blockId: number;
32
+ row: number;
33
+ col: number;
34
+ }
35
+ export interface RadioBindingArg extends InteractionCell {
36
+ value: string;
37
+ }
38
+ export interface MultiSelectBindingArg extends InteractionCell {
39
+ value: string;
40
+ }
41
+ export type PointAllocatorBindingArg = InteractionCell;
42
+ export type PercentAllocatorBindingArg = InteractionCell;
43
+ export interface NumberSliderBindingArg extends InteractionCell {
44
+ min: number;
45
+ max: number;
46
+ step?: number;
47
+ initialValue?: number;
48
+ }
49
+ export interface PointAllocationInfo {
50
+ blockId: number;
51
+ row: number;
52
+ col: number;
53
+ points: number;
54
+ }
55
+ /**
56
+ * Register + read surface. All methods are synchronous: the host holds
57
+ * overlay bindings and the user's selections in memory, and registration
58
+ * does not touch the workbook transaction/undo stack.
59
+ */
60
+ export interface CraftInteractionsApi {
61
+ registerRadio(binding: RadioBindingArg): void;
62
+ registerMultiSelect(binding: MultiSelectBindingArg): void;
63
+ setMultiSelectMax(groupId: string, max: number): void;
64
+ registerPointAllocator(binding: PointAllocatorBindingArg): void;
65
+ setPointPool(groupId: string, total: number): void;
66
+ registerPercentAllocator(binding: PercentAllocatorBindingArg): void;
67
+ registerNumberSlider(binding: NumberSliderBindingArg): void;
68
+ clearRadios(groupId?: string): void;
69
+ clearMultiSelects(groupId?: string): void;
70
+ clearPointAllocators(groupId?: string): void;
71
+ clearPercentAllocators(groupId?: string): void;
72
+ clearNumberSliders(groupId?: string): void;
73
+ getRadioSelection(groupId: string): string | undefined;
74
+ getMultiSelectSelections(groupId: string): readonly string[];
75
+ getPointPool(groupId: string): {
76
+ total: number;
77
+ used: number;
78
+ remaining: number;
79
+ };
80
+ getPointAllocations(groupId: string): readonly PointAllocationInfo[];
81
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * CraftInteractionsApi — the host capability surface for craft-defined
3
+ * cell-overlay widgets (radio groups, multi-select, point allocators,
4
+ * percent allocators, number sliders).
5
+ *
6
+ * Scope: BUILD + READ. This mirrors what the host injects into a craft
7
+ * iframe (`injectCraftInteractionAPIs`): a craft *registers* overlays onto
8
+ * block cells and *reads* what the user selected — it does NOT
9
+ * programmatically set selections (the user operates the overlay by
10
+ * clicking). Watson, being a craft that builds factory-simulator-like
11
+ * apps, needs exactly this build+read surface.
12
+ *
13
+ * Why an interface in logician (not an import of the host module):
14
+ * craft-interaction state is module singletons in the browser host
15
+ * (`src/core/craft-interactions`), which logician must not depend on —
16
+ * logician is platform-agnostic and reusable from a Node CLI / MCP server.
17
+ * The host implements this interface and passes it via
18
+ * `ToolContext.craftInteractions`; tools program against the interface
19
+ * only and report "not available" when it's absent.
20
+ *
21
+ * Method names and binding shapes mirror the injected window API 1:1, so
22
+ * the Watson host adapter is a thin pass-through:
23
+ * { registerRadio: window.registerRadio, ... }
24
+ */
25
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A {@link CraftInteractionsApi} implementation backed directly by
3
+ * logisheets-core's craft-interactions singletons.
4
+ *
5
+ * Use this when logician runs IN-PROCESS with the engine logic — i.e. a Node
6
+ * runtime (logisheets-runtime), or any non-iframe host. The browser/Watson
7
+ * path still uses the window-global adapter, because a craft iframe has its
8
+ * own module instances and must bridge to the *host page's* singletons across
9
+ * the iframe boundary; importing core there would yield a fresh, isolated
10
+ * store. In-process (Node), there is no boundary, so delegating straight to
11
+ * core is correct and removes the window dependency entirely.
12
+ */
13
+ import type { CraftInteractionsApi } from './craft-interactions-api.js';
14
+ export declare function createCoreCraftInteractions(): CraftInteractionsApi;