akanjs 3.0.0-beta.13 → 3.0.0-beta.15

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 (74) hide show
  1. package/common/CodeAgentClient.ts +102 -0
  2. package/common/CodeTranscript.ts +337 -0
  3. package/common/codeAgentProfile.ts +175 -0
  4. package/common/codeAgentWire.ts +409 -0
  5. package/common/index.ts +51 -0
  6. package/common/markdownSpans.ts +57 -0
  7. package/dictionary/agent.dictionary.ts +4 -11
  8. package/dictionary/base.dictionary.ts +1 -0
  9. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  10. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  11. package/package.json +3 -1
  12. package/server/akanOption.ts +9 -2
  13. package/server/di/predefinedAdaptor.ts +2 -2
  14. package/service/predefinedAdaptor/anthropicLlm.ts +6 -4
  15. package/service/predefinedAdaptor/index.ts +0 -1
  16. package/service/predefinedAdaptor/llm.adaptor.ts +21 -1
  17. package/service/predefinedAdaptor/openaiLlm.ts +37 -18
  18. package/store/agentic/StToolBuilder.ts +54 -0
  19. package/store/agentic/attachAgentic.ts +2 -1
  20. package/store/agentic/useFormTools.ts +1 -1
  21. package/types/common/CodeAgentClient.d.ts +26 -0
  22. package/types/common/CodeTranscript.d.ts +99 -0
  23. package/types/common/codeAgentProfile.d.ts +111 -0
  24. package/types/common/codeAgentWire.d.ts +388 -0
  25. package/types/common/index.d.ts +7 -0
  26. package/types/common/markdownSpans.d.ts +40 -0
  27. package/types/dictionary/agent.dictionary.d.ts +1 -1
  28. package/types/dictionary/base.dictionary.d.ts +1 -1
  29. package/types/dictionary/dictionary.d.ts +9 -9
  30. package/types/server/akanOption.d.ts +9 -2
  31. package/types/server/di/predefinedAdaptor.d.ts +2 -2
  32. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
  33. package/types/service/predefinedAdaptor/index.d.ts +0 -1
  34. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +14 -1
  35. package/types/service/predefinedAdaptor/openaiLlm.d.ts +20 -11
  36. package/types/store/agentic/StToolBuilder.d.ts +22 -0
  37. package/types/store/agentic/attachAgentic.d.ts +2 -1
  38. package/types/store/baseSt.d.ts +2 -2
  39. package/types/ui/Agent/Chat.d.ts +7 -1
  40. package/types/ui/Agent/Composer.d.ts +17 -1
  41. package/types/ui/Agent/MentionNode.d.ts +26 -0
  42. package/types/ui/Agent/RichInput.d.ts +22 -0
  43. package/types/ui/Agent/ToolCard.d.ts +16 -0
  44. package/types/ui/Agent/markdownSpans.d.ts +1 -0
  45. package/types/ui/Agent/mentionDraft.d.ts +19 -0
  46. package/types/ui/Agent/useChatReferences.d.ts +3 -2
  47. package/types/ui/UiOverride/context.d.ts +2 -0
  48. package/types/ui/index.d.ts +2 -1
  49. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  50. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  51. package/types/vendor/use-agentic/AgentSession.d.ts +15 -1
  52. package/types/vendor/use-agentic/ToolRunner.d.ts +21 -1
  53. package/types/vendor/use-agentic/types.d.ts +31 -1
  54. package/ui/Agent/Chat.tsx +24 -7
  55. package/ui/Agent/Composer.tsx +73 -21
  56. package/ui/Agent/Markdown.tsx +2 -2
  57. package/ui/Agent/MentionNode.ts +62 -0
  58. package/ui/Agent/RichInput.tsx +154 -0
  59. package/ui/Agent/ToolCard.tsx +39 -0
  60. package/ui/Agent/markdownSpans.tsx +26 -36
  61. package/ui/Agent/mentionDraft.ts +101 -0
  62. package/ui/Agent/useChatReferences.ts +5 -8
  63. package/ui/UiOverride/context.ts +2 -0
  64. package/ui/index.ts +2 -1
  65. package/vendor/use-agentic/AgentSession.ts +45 -1
  66. package/vendor/use-agentic/AgenticSurface.ts +2 -0
  67. package/vendor/use-agentic/ToolRunner.ts +55 -1
  68. package/vendor/use-agentic/types.ts +36 -1
  69. package/service/predefinedAdaptor/deepseekLlm.ts +0 -82
  70. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +0 -19
  71. /package/{ui/Agent → common}/markdownBlocks.ts +0 -0
  72. /package/{ui/Agent → common}/markdownTable.ts +0 -0
  73. /package/types/{ui/Agent → common}/markdownBlocks.d.ts +0 -0
  74. /package/types/{ui/Agent → common}/markdownTable.d.ts +0 -0
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import {
3
+ $createLineBreakNode,
4
+ $createParagraphNode,
5
+ $createTextNode,
6
+ $getRoot,
7
+ $getSelection,
8
+ $isElementNode,
9
+ $isRangeSelection,
10
+ $isRootNode,
11
+ $isTextNode,
12
+ type LexicalNode,
13
+ } from "lexical";
14
+ import { Reference } from "../../vendor/use-agentic";
15
+ import { $createMentionNode, $isMentionNode } from "./MentionNode";
16
+
17
+ /**
18
+ * The two directions between the composer's draft string and what the editor holds. The string stays the source
19
+ * of truth for the rest of the chat — it is what carries the `@[…](mention:…)` tokens onto the message — and the
20
+ * editor is one way of drawing it, so every offset here is an offset into **that string**, never into what the
21
+ * screen shows.
22
+ */
23
+ export class MentionDraft {
24
+ /** Runs inside an editor read or update, like every `$` function. */
25
+ static read(): string {
26
+ return $getRoot().getTextContent();
27
+ }
28
+
29
+ /** Rebuilds the whole content from a draft string — the path every write that is not a keystroke takes. */
30
+ static write(text: string, caretAt?: number) {
31
+ const paragraph = $createParagraphNode();
32
+ paragraph.append(...MentionDraft.nodesOf(text));
33
+ $getRoot().clear().append(paragraph);
34
+ MentionDraft.place(caretAt ?? text.length);
35
+ }
36
+
37
+ static nodesOf(text: string): LexicalNode[] {
38
+ const nodes: LexicalNode[] = [];
39
+ let at = 0;
40
+
41
+ const pattern = new RegExp(Reference.pattern.source, "g");
42
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
43
+ const [token, label, refName, refId, path] = match;
44
+ if (match.index > at) nodes.push(...MentionDraft.#plain(text.slice(at, match.index)));
45
+ nodes.push($createMentionNode({ refName, refId, label, ...(path ? { path } : {}) }));
46
+ at = match.index + token.length;
47
+ }
48
+ if (at < text.length) nodes.push(...MentionDraft.#plain(text.slice(at)));
49
+ return nodes;
50
+ }
51
+
52
+ /** Where the caret sits in the draft string, or `null` when there is no collapsed caret to report. */
53
+ static caret(): number | null {
54
+ const selection = $getSelection();
55
+ if (!$isRangeSelection(selection)) return null;
56
+ const anchor = selection.anchor;
57
+ const node = anchor.getNode();
58
+ if ($isTextNode(node)) {
59
+
60
+ const inner = $isMentionNode(node) ? (anchor.offset ? node.getTextContent().length : 0) : anchor.offset;
61
+ return MentionDraft.#before(node) + inner;
62
+ }
63
+ const children = $isElementNode(node) ? node.getChildren() : [];
64
+ const within = children.slice(0, anchor.offset).reduce((sum, child) => sum + child.getTextContent().length, 0);
65
+ return MentionDraft.#before(node) + within;
66
+ }
67
+
68
+ /** Puts the caret at a draft-string offset. Past the end, or inside a mention, it lands on the nearest edge. */
69
+ static place(at: number) {
70
+ const paragraph = $getRoot().getLastChild();
71
+ if (!$isElementNode(paragraph)) return;
72
+ let seen = 0;
73
+ for (const child of paragraph.getChildren()) {
74
+ const length = child.getTextContent().length;
75
+ if (at <= seen + length) {
76
+
77
+ if ($isTextNode(child) && !$isMentionNode(child)) child.select(at - seen, at - seen);
78
+ else if (at > seen) child.selectNext(0, 0);
79
+ else if (child.getPreviousSibling()) child.selectPrevious();
80
+ else paragraph.select(0, 0);
81
+ return;
82
+ }
83
+ seen += length;
84
+ }
85
+ paragraph.selectEnd();
86
+ }
87
+
88
+ static #plain(text: string): LexicalNode[] {
89
+ return text
90
+ .split("\n")
91
+ .flatMap((line, idx) => [...(idx ? [$createLineBreakNode()] : []), ...(line ? [$createTextNode(line)] : [])]);
92
+ }
93
+
94
+ static #before(node: LexicalNode): number {
95
+ let sum = 0;
96
+ for (let cursor = node.getPreviousSibling(); cursor; cursor = cursor.getPreviousSibling())
97
+ sum += cursor.getTextContent().length;
98
+ const parent = node.getParent();
99
+ return parent && !$isRootNode(parent) ? sum + MentionDraft.#before(parent) : sum;
100
+ }
101
+ }
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
  import { type RefObject, useEffect, useMemo, useRef } from "react";
3
3
  import { type AgentSession, type MessageReference, Reference } from "../../vendor/use-agentic";
4
+ import type { ComposerHandle } from "./Composer";
4
5
 
5
6
  interface ChatReferencesSetup {
6
7
  session: AgentSession;
7
8
  draft: string;
8
9
  /** The chat's own version snapshot — a `refer` from anywhere on the page is one of the changes it counts. */
9
10
  version: number;
10
- inputRef: RefObject<HTMLTextAreaElement | null>;
11
+ handleRef: RefObject<ComposerHandle | null>;
11
12
  onDraft: (text: string) => void;
12
13
  }
13
14
 
@@ -21,16 +22,15 @@ interface ChatReferencesSetup {
21
22
  * whose value was never staged (pasted out of an earlier message) travels as a pointer with a note saying to read
22
23
  * it again, which is the same shape a restored conversation produces.
23
24
  */
24
- export const useChatReferences = ({ session, draft, version, inputRef, onDraft }: ChatReferencesSetup) => {
25
+ export const useChatReferences = ({ session, draft, version, handleRef, onDraft }: ChatReferencesSetup) => {
25
26
  const caret = useRef<number | null>(null);
26
27
 
27
28
  useEffect(() => session.attachChat(), [session]);
28
29
  useEffect(() => {
29
30
  const pending = session.pendingInserts;
30
31
  if (!pending.length) return;
31
- const area = inputRef.current;
32
32
 
33
- const at = area ? area.selectionStart : draft.length;
33
+ const at = handleRef.current?.caret() ?? draft.length;
34
34
  const head = draft.slice(0, at);
35
35
  const tail = draft.slice(at);
36
36
  const tokens = pending.map((one) => Reference.token(one)).join(" ");
@@ -44,11 +44,8 @@ export const useChatReferences = ({ session, draft, version, inputRef, onDraft }
44
44
  const at = caret.current;
45
45
  if (at === null) return;
46
46
  caret.current = null;
47
- const area = inputRef.current;
48
- if (!area) return;
49
47
 
50
- area.focus();
51
- area.setSelectionRange(at, at);
48
+ handleRef.current?.setCaret(at);
52
49
  }, [draft]);
53
50
  const references = useMemo<MessageReference[]>(() => {
54
51
  const staged = new Map(session.staged.map((one) => [Reference.keyOf(one), one]));
@@ -12,6 +12,7 @@ import type { MenuProps as AgentMenuProps } from "../Agent/Menu";
12
12
  import type { QuestionProps as AgentQuestionProps } from "../Agent/Question";
13
13
  import type { QueuedProps as AgentQueuedProps } from "../Agent/Queued";
14
14
  import type { StepsProps as AgentStepsProps } from "../Agent/Steps";
15
+ import type { ToolCardProps as AgentToolCardProps } from "../Agent/ToolCard";
15
16
  import type { BadgeProps } from "../Badge";
16
17
  import type { ButtonProps } from "../Button";
17
18
  import type { DatePickerProps, RangePickerProps, TimePickerProps } from "../DatePicker";
@@ -74,6 +75,7 @@ export interface AkanUiOverrides {
74
75
  AgentQueued: ComponentType<AgentQueuedProps>;
75
76
  AgentMenu: ComponentType<AgentMenuProps>;
76
77
  AgentMarkdown: ComponentType<AgentMarkdownProps>;
78
+ AgentToolCard: ComponentType<AgentToolCardProps>;
77
79
  AgentCode: ComponentType<AgentCodeProps>;
78
80
 
79
81
  Button: ComponentType<ButtonProps<unknown>>;
package/ui/index.ts CHANGED
@@ -36,7 +36,7 @@ export {
36
36
  export { type BubbleProps, DefaultBubble } from "./Agent/Bubble";
37
37
  export type { ChatProps } from "./Agent/Chat";
38
38
  export { type ChatCommand, ChatCommands } from "./Agent/ChatCommands";
39
- export { type ComposerProps, DefaultComposer } from "./Agent/Composer";
39
+ export { type ComposerHandle, type ComposerProps, DefaultComposer } from "./Agent/Composer";
40
40
  export { fetchRunner } from "./Agent/fetchRunner";
41
41
  export type { HistoryProps as AgentHistoryProps } from "./Agent/History";
42
42
  export { DefaultLauncher, type LauncherProps } from "./Agent/Launcher";
@@ -51,6 +51,7 @@ export {
51
51
  export { DefaultSteps, type StepsProps } from "./Agent/Steps";
52
52
  export type { PersistOption } from "./Agent/sessionHistory";
53
53
  export type { AgentBuiltin, BuiltinOption } from "./Agent/sessionView";
54
+ export { DefaultToolCard, type ToolCardProps } from "./Agent/ToolCard";
54
55
  export { tokenCount } from "./Agent/tokenCount";
55
56
  export type { QueuedMessage } from "./Agent/useChatQueue";
56
57
  export type { ReferenceCandidate, ReferenceSource } from "./Agent/useReferenceMenu";
@@ -2,7 +2,7 @@ import type { AgentProgressReport } from "./AgentProgress";
2
2
  import { Compaction, type CompactOptions } from "./Compaction";
3
3
  import { Reference } from "./Reference";
4
4
  import { ToolOutput } from "./ToolOutput";
5
- import { type ToolApprovalRequest, ToolRunner } from "./ToolRunner";
5
+ import { type ToolApprovalRequest, type ToolCardAnswer, type ToolCardRequest, ToolRunner } from "./ToolRunner";
6
6
  import { Transcript } from "./Transcript";
7
7
  import type {
8
8
  AgentRunner,
@@ -15,6 +15,7 @@ import type {
15
15
  ToolActivity,
16
16
  ToolCallRequest,
17
17
  ToolCallResult,
18
+ ToolCard,
18
19
  TurnStop,
19
20
  } from "./types";
20
21
 
@@ -40,6 +41,20 @@ export interface PendingQuestion {
40
41
  dismiss: (reason?: string) => void;
41
42
  }
42
43
 
44
+ /**
45
+ * A call parked on a component the app declared with the tool — a form to fill in, a choice no list of strings
46
+ * could carry. The loop waits on it exactly as it waits on an approval, and `render` is the app's own, so the
47
+ * framework decides where the card sits and nothing about what it asks.
48
+ */
49
+ export interface PendingCard {
50
+ callId: string;
51
+ name: string;
52
+ args: Record<string, unknown>;
53
+ render: ToolCard;
54
+ submit: (value: unknown) => void;
55
+ dismiss: (reason?: string) => void;
56
+ }
57
+
43
58
  /**
44
59
  * Where a session keeps its transcript across page loads. Storage-neutral: the host decides what backs it, so a
45
60
  * server-side transcript is as legal as web storage — which is why every method may answer asynchronously. A
@@ -150,6 +165,7 @@ export class AgentSession {
150
165
  #running = false;
151
166
  #pending: PendingApproval | null = null;
152
167
  #question: PendingQuestion | null = null;
168
+ #card: PendingCard | null = null;
153
169
  #staged: MessageReference[] = [];
154
170
  #inserts: MessageReference[] = [];
155
171
  #chats = 0;
@@ -174,6 +190,7 @@ export class AgentSession {
174
190
  this.#options = options;
175
191
  this.#tools = new ToolRunner(surface, {
176
192
  approve: (request, signal) => this.#awaitApproval(request, signal),
193
+ card: (request, signal) => this.#awaitCard(request, signal),
177
194
  settle: () => this.#options.settle?.(),
178
195
  activity: (event) => this.#options.onActivity?.(event),
179
196
  progress: ({ callId, report }) => {
@@ -234,6 +251,10 @@ export class AgentSession {
234
251
  return this.#question;
235
252
  }
236
253
 
254
+ get pendingCard(): PendingCard | null {
255
+ return this.#card;
256
+ }
257
+
237
258
  /**
238
259
  * What the user has pointed at and not yet sent.
239
260
  *
@@ -426,6 +447,7 @@ export class AgentSession {
426
447
  this.#active = null;
427
448
  this.#pending = null;
428
449
  this.#question = null;
450
+ this.#card = null;
429
451
  this.#progress = null;
430
452
 
431
453
  const draft = this.#messages[this.#messages.length - 1];
@@ -712,6 +734,28 @@ export class AgentSession {
712
734
  });
713
735
  }
714
736
 
737
+ #awaitCard(request: ToolCardRequest, signal: AbortSignal): Promise<ToolCardAnswer> {
738
+ return new Promise((resolve) => {
739
+ const settle = (answer: ToolCardAnswer) => {
740
+ this.#card = null;
741
+ signal.removeEventListener("abort", onAbort);
742
+ this.#notify();
743
+ resolve(answer);
744
+ };
745
+ const onAbort = () => settle({ error: "The user aborted the turn." });
746
+ signal.addEventListener("abort", onAbort);
747
+ this.#card = {
748
+ callId: request.callId,
749
+ name: request.name,
750
+ args: request.args,
751
+ render: request.render,
752
+ submit: (value) => settle({ result: value }),
753
+ dismiss: (reason) => settle({ error: reason ?? "The user closed the card without filling it in." }),
754
+ };
755
+ this.#notify();
756
+ });
757
+ }
758
+
715
759
  #awaitApproval(request: ToolApprovalRequest, signal: AbortSignal): Promise<true | string> {
716
760
  return new Promise((resolve) => {
717
761
  const settle = (value: true | string) => {
@@ -183,6 +183,8 @@ export class AgenticSurface {
183
183
  try {
184
184
  const verdict = entry.guard?.(args) ?? true;
185
185
  if (verdict !== true) throw new Error(verdict);
186
+
187
+ if (!entry.run) throw new Error(`${name} is answered by the user, and this caller cannot ask them.`);
186
188
  return await entry.run(args);
187
189
  } catch (error) {
188
190
  record.error = error instanceof Error ? error.message : String(error);
@@ -1,7 +1,15 @@
1
1
  import { AgentAbort } from "./AgentAbort";
2
2
  import { AgentProgress, type AgentProgressReport } from "./AgentProgress";
3
3
  import { ToolOutput } from "./ToolOutput";
4
- import type { SurfaceView, ToolActivity, ToolCallRequest, ToolCallResult, ToolEntry } from "./types";
4
+ import type {
5
+ SurfaceView,
6
+ ToolActivity,
7
+ ToolCallRequest,
8
+ ToolCallResult,
9
+ ToolCard,
10
+ ToolCardEntry,
11
+ ToolEntry,
12
+ } from "./types";
5
13
 
6
14
  export interface ToolApprovalRequest {
7
15
  callId: string;
@@ -11,6 +19,17 @@ export interface ToolApprovalRequest {
11
19
  message: string;
12
20
  }
13
21
 
22
+ /** A call whose answer the user writes, handed to the host with the component its declaration named. */
23
+ export interface ToolCardRequest {
24
+ callId: string;
25
+ name: string;
26
+ args: Record<string, unknown>;
27
+ render: ToolCard;
28
+ }
29
+
30
+ /** What the card settled on: the value the model reads back, or why there is none. */
31
+ export type ToolCardAnswer = { result: unknown } | { error: string };
32
+
14
33
  /** `report` is `null` once the call is over, carrying the id so a host can ignore a clear that is not its own. */
15
34
  export interface ToolProgress {
16
35
  callId: string;
@@ -30,6 +49,13 @@ export interface ToolRunnerHost {
30
49
  * a host that may not perform the action, and the refusal says so rather than silently downgrading the gate.
31
50
  */
32
51
  approve?: (request: ToolApprovalRequest, signal: AbortSignal) => Promise<true | string>;
52
+ /**
53
+ * Parks the call until the user fills in the card its declaration named, and answers with what they submitted.
54
+ *
55
+ * Omitting it refuses those calls rather than running something in their place: a card tool has no function to
56
+ * fall back to — the user *is* the implementation — so a host with nowhere to render one may not answer it.
57
+ */
58
+ card?: (request: ToolCardRequest, signal: AbortSignal) => Promise<ToolCardAnswer>;
33
59
  /**
34
60
  * Awaited after a tool that changed something and before its change report is taken. A surface is read
35
61
  * synchronously and a screen does not settle synchronously, so without this the report describes the moment
@@ -98,6 +124,7 @@ export class ToolRunner {
98
124
  const fallback = this.#host.fallback;
99
125
  return fallback ? await fallback(call, signal) : { ...base, error: `Unknown tool: ${call.name}` };
100
126
  }
127
+ if (entry.card) return await this.#carded(call, entry, signal);
101
128
  const message = ToolRunner.confirmMessage(call.name, entry, call.args);
102
129
  if (message) {
103
130
  const approve = this.#host.approve;
@@ -109,6 +136,33 @@ export class ToolRunner {
109
136
  return await ToolRunner.#serialized(() => this.#execute(call, entry, signal));
110
137
  }
111
138
 
139
+ /**
140
+ * A call the user answers. It waits **outside** the serialization queue, for the reason an approval does: a card
141
+ * parked in front of somebody is not work, and holding the lock across it would let one agent's unanswered form
142
+ * freeze every other agent on the page.
143
+ *
144
+ * The screen is still snapshotted around the wait, because a card that writes what it collected into a store is
145
+ * the ordinary case and the model has to be told what moved. Nothing is drawn through `activity` — that draws a
146
+ * call landing *on* the page, and this one lands in the chat.
147
+ */
148
+ async #carded(call: ToolCallRequest, entry: ToolCardEntry, signal: AbortSignal): Promise<ToolCallResult> {
149
+ const base = { id: call.id, name: call.name };
150
+ const verdict = entry.guard?.(call.args) ?? true;
151
+ if (verdict !== true) return { ...base, error: verdict };
152
+ const ask = this.#host.card;
153
+ if (!ask) return { ...base, error: `${call.name} is answered by the user, and nothing here can ask them.` };
154
+ const before = this.#surface.snapshot();
155
+ const answered = await ask({ callId: call.id, name: call.name, args: call.args, render: entry.card }, signal);
156
+ if ("error" in answered) return { ...base, error: answered.error };
157
+ if (entry.settle !== false) await this.#host.settle?.();
158
+ const changes = this.#surface.diffSince(before);
159
+ return {
160
+ ...base,
161
+ ...(answered.result !== undefined ? { result: answered.result } : {}),
162
+ ...(changes.length ? { changes } : {}),
163
+ };
164
+ }
165
+
112
166
  async #execute(call: ToolCallRequest, entry: ToolEntry, signal: AbortSignal): Promise<ToolCallResult> {
113
167
  const base = { id: call.id, name: call.name };
114
168
  const before = this.#surface.snapshot();
@@ -1,3 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+
1
3
  export type JsonSchema = Record<string, unknown>;
2
4
 
3
5
  /** `true` asks with a default message, a string is the message, a function decides from the arguments. */
@@ -6,7 +8,24 @@ export type ToolConfirm = boolean | string | ((args: Record<string, unknown>) =>
6
8
  /** Re-checked at the moment of execution; a string is the refusal reason the agent reads. */
7
9
  export type ToolGuard = (args: Record<string, unknown>) => true | string;
8
10
 
9
- export interface ToolEntry {
11
+ /**
12
+ * What a card tool renders while its call waits, and the two ways the user ends that wait. `submit` is the call's
13
+ * result, `cancel` is its refusal; whichever comes first settles the call and takes the card off the screen, so a
14
+ * second call of either does nothing.
15
+ */
16
+ export interface ToolCardControl {
17
+ args: Record<string, unknown>;
18
+ submit: (value: unknown) => void;
19
+ cancel: (reason?: string) => void;
20
+ }
21
+
22
+ /**
23
+ * Called, not mounted — the host invokes it inside its own render, so the returned tree keeps no state of its
24
+ * own between renders. Put anything stateful in a component the function returns.
25
+ */
26
+ export type ToolCard = (control: ToolCardControl) => ReactNode;
27
+
28
+ interface ToolEntryBase {
10
29
  name: string;
11
30
  description?: string;
12
31
  parameters?: JsonSchema;
@@ -17,9 +36,25 @@ export interface ToolEntry {
17
36
  settle?: boolean;
18
37
  confirm?: ToolConfirm;
19
38
  guard?: ToolGuard;
39
+ }
40
+
41
+ export interface ToolActionEntry extends ToolEntryBase {
20
42
  run: (args: Record<string, unknown>) => unknown;
43
+ card?: never;
44
+ }
45
+
46
+ /**
47
+ * A call the **user** answers rather than the screen: the host parks the call, renders `card`, and what the card
48
+ * submits is what the model reads back. `confirm` is not read for one — the card in front of the user is already
49
+ * the asking, and a gate before it would ask them twice for one thing.
50
+ */
51
+ export interface ToolCardEntry extends ToolEntryBase {
52
+ card: ToolCard;
53
+ run?: never;
21
54
  }
22
55
 
56
+ export type ToolEntry = ToolActionEntry | ToolCardEntry;
57
+
23
58
  /**
24
59
  * That a call is happening, for a host drawing it on the screen rather than in a transcript.
25
60
  *
@@ -1,82 +0,0 @@
1
- import { Err } from "akanjs/dictionary";
2
- import { adapt } from "../adapt";
3
- import type { LlmAdaptor, LlmOption, LlmTurnAnswer, LlmTurnRequest } from "./llm.adaptor";
4
- import { type OpenaiAnswer, OpenaiDialect } from "./openaiDialect";
5
-
6
- /**
7
- * The framework's default provider, and the one an app gets without choosing.
8
- *
9
- * `accepts` is left undeclared, so by the time an attachment reaches the dialect `AgentService.readable` has
10
- * reduced it to its text and turned everything else into a note. That is deliberate rather than pending: DeepSeek's
11
- * chat API is text, and an adaptor that claimed otherwise would hand it bytes it answers about having never seen.
12
- * An app that wants vision swaps the role — `option.applyAdaptor(LlmAdaptorRole, OpenaiLlm)` or `AnthropicLlm`.
13
- */
14
- export class DeepseekLlm
15
- extends adapt("deepseekLlm" as const, ({ use }) => ({
16
- llmOption: use<LlmOption>(),
17
- }))
18
- implements LlmAdaptor
19
- {
20
- get #model() {
21
- return this.llmOption.model ?? "deepseek-v4-flash";
22
- }
23
- get #host() {
24
- return this.llmOption.host ?? "https://api.deepseek.com";
25
- }
26
-
27
- async chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null> {
28
- if (!this.llmOption.apiKey) {
29
- this.logger.warn("No LLM API key is configured — set one with option.setLlm(). Agent turns are unavailable.");
30
- return null;
31
- }
32
- try {
33
- if (!onDelta) {
34
- const answer = await this.#api<OpenaiAnswer>(
35
- "/chat/completions",
36
- OpenaiDialect.requestBody(this.#model, request),
37
- );
38
- return OpenaiDialect.turnAnswer(answer);
39
- }
40
- const body = await this.#apiStream(
41
- "/chat/completions",
42
- OpenaiDialect.requestBody(this.#model, request, { stream: true }),
43
- );
44
- return await OpenaiDialect.consumeStream(body, onDelta);
45
- } catch (error) {
46
-
47
- this.logger.error(`DeepSeek turn failed: ${error instanceof Error ? error.message : String(error)}`);
48
- throw error;
49
- }
50
- }
51
-
52
- async #api<T>(path: string, body: object): Promise<T> {
53
- const response = await fetch(`${this.#host}${path}`, {
54
- method: "POST",
55
- headers: { "content-type": "application/json", authorization: `Bearer ${this.llmOption.apiKey}` },
56
- body: JSON.stringify(body),
57
-
58
- signal: AbortSignal.timeout(120_000),
59
- });
60
- if (!response.ok) throw await DeepseekLlm.refusal(response);
61
- return (await response.json()) as T;
62
- }
63
-
64
- async #apiStream(path: string, body: object): Promise<ReadableStream<Uint8Array>> {
65
- const response = await fetch(`${this.#host}${path}`, {
66
- method: "POST",
67
- headers: { "content-type": "application/json", authorization: `Bearer ${this.llmOption.apiKey}` },
68
- body: JSON.stringify(body),
69
- signal: AbortSignal.timeout(120_000),
70
- });
71
- if (!response.ok || !response.body) throw await DeepseekLlm.refusal(response);
72
- return response.body;
73
- }
74
-
75
- /** Carried on the `Err` so the chat prints the provider's own sentence rather than a status number. */
76
- static async refusal(response: Response): Promise<Error> {
77
- return new Err("agent.error.deepseekRequestFailed", {
78
- status: String(response.status),
79
- reason: await OpenaiDialect.reasonOf(response),
80
- });
81
- }
82
- }
@@ -1,19 +0,0 @@
1
- import type { LlmAdaptor, LlmOption, LlmTurnAnswer, LlmTurnRequest } from "./llm.adaptor";
2
- declare const DeepseekLlm_base: import("..").AdaptorCls<{}, {
3
- llmOption: import("..").InjectInfo<"use", LlmOption, never, never>;
4
- }>;
5
- /**
6
- * The framework's default provider, and the one an app gets without choosing.
7
- *
8
- * `accepts` is left undeclared, so by the time an attachment reaches the dialect `AgentService.readable` has
9
- * reduced it to its text and turned everything else into a note. That is deliberate rather than pending: DeepSeek's
10
- * chat API is text, and an adaptor that claimed otherwise would hand it bytes it answers about having never seen.
11
- * An app that wants vision swaps the role — `option.applyAdaptor(LlmAdaptorRole, OpenaiLlm)` or `AnthropicLlm`.
12
- */
13
- export declare class DeepseekLlm extends DeepseekLlm_base implements LlmAdaptor {
14
- #private;
15
- chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
16
- /** Carried on the `Err` so the chat prints the provider's own sentence rather than a status number. */
17
- static refusal(response: Response): Promise<Error>;
18
- }
19
- export {};
File without changes
File without changes
File without changes
File without changes