akanjs 3.0.0-alpha.55 → 3.0.0-alpha.56
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/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/service/agent.service.ts +5 -2
- package/service/toolNames.ts +119 -0
- package/types/service/toolNames.d.ts +35 -0
- package/types/ui/Agent/Context.d.ts +6 -2
- package/types/ui/Agent/Zone.d.ts +4 -0
- package/types/ui/Field.d.ts +12 -12
- package/types/ui/ToggleSelect.d.ts +1 -0
- package/types/vendor/use-agentic/index.d.ts +1 -0
- package/types/vendor/use-agentic/sharedContext.d.ts +15 -0
- package/ui/Agent/Context.tsx +13 -4
- package/ui/Agent/Zone.tsx +4 -0
- package/ui/Field.tsx +41 -34
- package/ui/ToggleSelect.tsx +4 -1
- package/vendor/use-agentic/index.ts +1 -0
- package/vendor/use-agentic/sharedContext.ts +25 -0
- package/vendor/use-agentic/surfaceContext.ts +4 -3
- package/vendor/use-agentic/useAgent.ts +3 -2
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/service/agent.service.ts
CHANGED
|
@@ -7,15 +7,18 @@ import type {
|
|
|
7
7
|
} from "./predefinedAdaptor/llm.adaptor";
|
|
8
8
|
import { LlmAdaptorRole } from "./predefinedAdaptor/role.adaptor";
|
|
9
9
|
import { serve } from "./serve";
|
|
10
|
+
import { ToolNames } from "./toolNames";
|
|
10
11
|
|
|
11
12
|
export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
12
13
|
llm: plug(LlmAdaptorRole),
|
|
13
14
|
})) {
|
|
14
15
|
async runTurn(request: LlmTurnRequest, onDelta?: (delta: string) => void) {
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
const names = ToolNames.of(request);
|
|
18
|
+
const prepared = names.encode(AgentService.readable(AgentService.explained(request), this.llm.accepts));
|
|
16
19
|
const answer = await this.llm.chat(prepared, onDelta);
|
|
17
20
|
if (!answer) throw new Err("agent.error.llmUnavailable");
|
|
18
|
-
return { text: answer.text ?? "", toolCalls: answer.toolCalls ?? [], stop: answer.stop };
|
|
21
|
+
return { text: answer.text ?? "", toolCalls: names.decode(answer.toolCalls ?? []), stop: answer.stop };
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { AgentWireMessage, AgentWireToolCall, LlmTurnRequest } from "./predefinedAdaptor/llm.adaptor";
|
|
2
|
+
|
|
3
|
+
const wireSafe = /^[A-Za-z0-9_-]+$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renames a turn's tools onto what a provider's function-calling wire accepts, and reads the answer back.
|
|
7
|
+
*
|
|
8
|
+
* A zone publishes its tools scope-prefixed — `videoProjectDraft.createVideoProject` — which is legal for MCP,
|
|
9
|
+
* where `.` is an allowed character and the scope join. Every OpenAI-compatible and Anthropic function schema is
|
|
10
|
+
* narrower: `[A-Za-z0-9_-]`, at most 64 characters. A provider that validates answers 400; DeepSeek does not, and
|
|
11
|
+
* what happened instead was worse to debug — the model normalized the illegal name itself, called the bare
|
|
12
|
+
* `createVideoProject`, and the browser answered `Unknown tool`, spending a turn on a tool that was published all
|
|
13
|
+
* along.
|
|
14
|
+
*
|
|
15
|
+
* Renamed here rather than in each adaptor for the reason `AgentService.explained` gives: every adaptor would
|
|
16
|
+
* otherwise have to remember, and forgetting is silent. A name the wire already accepts is left alone, so the
|
|
17
|
+
* root agent's request is byte-for-byte what it was.
|
|
18
|
+
*/
|
|
19
|
+
export class ToolNames {
|
|
20
|
+
/** Both dialects reject a longer name, and neither says so in terms of the tool you wrote. */
|
|
21
|
+
static readonly limit = 64;
|
|
22
|
+
|
|
23
|
+
readonly #toWire = new Map<string, string>();
|
|
24
|
+
readonly #toSurface = new Map<string, string>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every name the request carries, not just the published ones: the transcript holds calls to tools that have
|
|
28
|
+
* since left the screen, and one of those reaching the wire unrenamed is the same failure a turn later.
|
|
29
|
+
*/
|
|
30
|
+
static of(request: LlmTurnRequest): ToolNames {
|
|
31
|
+
return new ToolNames([
|
|
32
|
+
...request.tools.map((tool) => tool.name),
|
|
33
|
+
...request.messages.flatMap((message) => ToolNames.#namesIn(message)),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(names: Iterable<string>) {
|
|
38
|
+
const all = [...new Set(names)];
|
|
39
|
+
|
|
40
|
+
const taken = new Set(all.filter((name) => ToolNames.#fits(name)));
|
|
41
|
+
|
|
42
|
+
for (const name of all.filter((candidate) => !ToolNames.#fits(candidate)).sort((a, b) => (a < b ? -1 : 1))) {
|
|
43
|
+
const wire = ToolNames.#unique(ToolNames.#fold(name), taken);
|
|
44
|
+
taken.add(wire);
|
|
45
|
+
this.#toWire.set(name, wire);
|
|
46
|
+
this.#toSurface.set(wire, name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get renamed() {
|
|
51
|
+
return this.#toWire.size > 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
wire(name: string) {
|
|
55
|
+
return this.#toWire.get(name) ?? name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unknown stays as it came. A model that invented a name is answered by the surface's own `Unknown tool`, which
|
|
60
|
+
* lands in the transcript as a tool result it can correct from — guessing which tool it meant would run one.
|
|
61
|
+
*/
|
|
62
|
+
surface(name: string) {
|
|
63
|
+
return this.#toSurface.get(name) ?? name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
encode(request: LlmTurnRequest): LlmTurnRequest {
|
|
67
|
+
if (!this.renamed) return request;
|
|
68
|
+
return {
|
|
69
|
+
...request,
|
|
70
|
+
tools: request.tools.map((tool) => ({ ...tool, name: this.wire(tool.name) })),
|
|
71
|
+
messages: request.messages.map((message) => this.#encoded(message)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
decode(calls: AgentWireToolCall[]): AgentWireToolCall[] {
|
|
76
|
+
if (!this.renamed) return calls;
|
|
77
|
+
return calls.map((call) => ({ ...call, name: this.surface(call.name) }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#encoded(message: AgentWireMessage): AgentWireMessage {
|
|
81
|
+
if (!message.toolCalls?.length && !message.toolResults?.length) return message;
|
|
82
|
+
return {
|
|
83
|
+
...message,
|
|
84
|
+
...(message.toolCalls?.length
|
|
85
|
+
? { toolCalls: message.toolCalls.map((call) => ({ ...call, name: this.wire(call.name) })) }
|
|
86
|
+
: {}),
|
|
87
|
+
...(message.toolResults?.length
|
|
88
|
+
? { toolResults: message.toolResults.map((result) => ({ ...result, name: this.wire(result.name) })) }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static #namesIn(message: AgentWireMessage): string[] {
|
|
94
|
+
return [
|
|
95
|
+
...(message.toolCalls ?? []).map((call) => call.name),
|
|
96
|
+
...(message.toolResults ?? []).map((result) => result.name),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static #fits(name: string) {
|
|
101
|
+
return name.length <= ToolNames.limit && wireSafe.test(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `.` is the one character the surface itself adds, so it folds to the `__` every MCP client already reads. */
|
|
105
|
+
static #fold(name: string) {
|
|
106
|
+
const folded = name.replaceAll(".", "__").replace(/[^A-Za-z0-9_-]/g, "-");
|
|
107
|
+
|
|
108
|
+
return folded.length <= ToolNames.limit ? folded : folded.slice(folded.length - ToolNames.limit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static #unique(candidate: string, taken: Set<string>) {
|
|
112
|
+
if (!taken.has(candidate)) return candidate;
|
|
113
|
+
for (let idx = 2; ; idx += 1) {
|
|
114
|
+
const suffix = `_${idx}`;
|
|
115
|
+
const next = `${candidate.slice(0, ToolNames.limit - suffix.length)}${suffix}`;
|
|
116
|
+
if (!taken.has(next)) return next;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentWireToolCall, LlmTurnRequest } from "./predefinedAdaptor/llm.adaptor";
|
|
2
|
+
/**
|
|
3
|
+
* Renames a turn's tools onto what a provider's function-calling wire accepts, and reads the answer back.
|
|
4
|
+
*
|
|
5
|
+
* A zone publishes its tools scope-prefixed — `videoProjectDraft.createVideoProject` — which is legal for MCP,
|
|
6
|
+
* where `.` is an allowed character and the scope join. Every OpenAI-compatible and Anthropic function schema is
|
|
7
|
+
* narrower: `[A-Za-z0-9_-]`, at most 64 characters. A provider that validates answers 400; DeepSeek does not, and
|
|
8
|
+
* what happened instead was worse to debug — the model normalized the illegal name itself, called the bare
|
|
9
|
+
* `createVideoProject`, and the browser answered `Unknown tool`, spending a turn on a tool that was published all
|
|
10
|
+
* along.
|
|
11
|
+
*
|
|
12
|
+
* Renamed here rather than in each adaptor for the reason `AgentService.explained` gives: every adaptor would
|
|
13
|
+
* otherwise have to remember, and forgetting is silent. A name the wire already accepts is left alone, so the
|
|
14
|
+
* root agent's request is byte-for-byte what it was.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ToolNames {
|
|
17
|
+
#private;
|
|
18
|
+
/** Both dialects reject a longer name, and neither says so in terms of the tool you wrote. */
|
|
19
|
+
static readonly limit = 64;
|
|
20
|
+
/**
|
|
21
|
+
* Every name the request carries, not just the published ones: the transcript holds calls to tools that have
|
|
22
|
+
* since left the screen, and one of those reaching the wire unrenamed is the same failure a turn later.
|
|
23
|
+
*/
|
|
24
|
+
static of(request: LlmTurnRequest): ToolNames;
|
|
25
|
+
constructor(names: Iterable<string>);
|
|
26
|
+
get renamed(): boolean;
|
|
27
|
+
wire(name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Unknown stays as it came. A model that invented a name is answered by the surface's own `Unknown tool`, which
|
|
30
|
+
* lands in the transcript as a tool result it can correct from — guessing which tool it meant would run one.
|
|
31
|
+
*/
|
|
32
|
+
surface(name: string): string;
|
|
33
|
+
encode(request: LlmTurnRequest): LlmTurnRequest;
|
|
34
|
+
decode(calls: AgentWireToolCall[]): AgentWireToolCall[];
|
|
35
|
+
}
|
|
@@ -2,8 +2,12 @@ interface ContextProps {
|
|
|
2
2
|
className?: string;
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
|
-
* Assembles and shows
|
|
6
|
-
*
|
|
5
|
+
* Assembles and shows exactly what a turn would carry, on demand — the one preview of "what does the agent see on
|
|
6
|
+
* this screen" that no amount of reading the source answers.
|
|
7
|
+
*
|
|
8
|
+
* The tool list leads, by name only: a zone publishes its tools scope-prefixed, and instructions that name a tool
|
|
9
|
+
* without its prefix name a tool that does not exist. That is invisible in the source of either file and obvious
|
|
10
|
+
* here.
|
|
7
11
|
*/
|
|
8
12
|
export default function Context({ className }: ContextProps): import("react/jsx-runtime").JSX.Element;
|
|
9
13
|
export {};
|
package/types/ui/Agent/Zone.d.ts
CHANGED
|
@@ -40,5 +40,9 @@ export interface ZoneProps {
|
|
|
40
40
|
* inside — hook tools, `st.use` subscriptions, guides — belongs to this zone's session *and* to the root agent:
|
|
41
41
|
* zones are views, never walls. An `Agent.Chat` mounted inside binds to this session automatically, so two zones
|
|
42
42
|
* on one screen run two conversations in parallel, each seeing only its own subtree.
|
|
43
|
+
*
|
|
44
|
+
* **Everything a zone publishes is named `<id>.<name>`.** Instructions that name a tool must carry the prefix —
|
|
45
|
+
* a bare name is a tool that does not exist, and the model calling it spends a turn on `Unknown tool`. Build the
|
|
46
|
+
* name from the id rather than writing it twice, and read `Agent.Context`'s Assemble to see the published list.
|
|
43
47
|
*/
|
|
44
48
|
export declare const Zone: ({ className, id, label, instructions, runner, maxTurns, compact, builtins, persist, onCompact, session: provided, onSession, children, }: ZoneProps) => import("react/jsx-runtime").JSX.Element;
|
package/types/ui/Field.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare const Field: {
|
|
|
33
33
|
Price: import("react").MemoExoticComponent<({ label, desc, labelClassName, className, value, onChange, placeholder, nullable, disabled, minlength, maxlength, transform, validate, onPressEnter, inputClassName, inputStyleType, }: PriceProps) => import("react/jsx-runtime").JSX.Element>;
|
|
34
34
|
TextArea: import("react").MemoExoticComponent<({ label, desc, labelClassName, className, value, onChange, placeholder, nullable, disabled, rows, minlength, maxlength, transform, validate, onPressEnter, cache, inputClassName, }: TextAreaProps) => import("react/jsx-runtime").JSX.Element>;
|
|
35
35
|
Switch: ({ label, desc, labelClassName, className, value, onChange, disabled, inputClassName, onDesc, offDesc, }: SwitchProps) => import("react/jsx-runtime").JSX.Element;
|
|
36
|
-
ToggleSelect: <I extends string | number | boolean | null>({ className, labelClassName, label, desc, items, value, validate, onChange, nullable, disabled, btnClassName, }: ToggleSelectProps<I>) => import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
ToggleSelect: <I extends string | number | boolean | null, Nullable extends boolean = false>({ className, labelClassName, label, desc, items, value, validate, onChange, nullable, disabled, btnClassName, }: ToggleSelectProps<I, Nullable>) => import("react/jsx-runtime").JSX.Element;
|
|
37
37
|
MultiToggleSelect: <I extends string | number | boolean>({ className, labelClassName, label, desc, items, value, minlength, maxlength, validate, onChange, disabled, }: MultiToggleSelectProps<I>) => import("react/jsx-runtime").JSX.Element;
|
|
38
38
|
TextList: ({ label, desc, labelClassName, className, value, onChange, placeholder, disabled, transform, minlength, maxlength, minTextlength, maxTextlength, cache, validate, inputClassName, }: TextListProps) => import("react/jsx-runtime").JSX.Element;
|
|
39
39
|
Tags: ({ label, desc, labelClassName, className, value, onChange, placeholder, disabled, transform, minlength, maxlength, minTextlength, maxTextlength, validate, inputClassName, }: TagsProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -71,7 +71,7 @@ interface ListProps<Item> {
|
|
|
71
71
|
label?: string;
|
|
72
72
|
desc?: string;
|
|
73
73
|
nullable?: boolean;
|
|
74
|
-
value: Item[];
|
|
74
|
+
value: Item[] | null;
|
|
75
75
|
onChange: (value: Item[]) => void;
|
|
76
76
|
onAdd: () => void;
|
|
77
77
|
renderItem: (item: Item, idx: number) => ReactNode;
|
|
@@ -137,14 +137,14 @@ interface SwitchProps {
|
|
|
137
137
|
desc?: string;
|
|
138
138
|
labelClassName?: string;
|
|
139
139
|
className?: string;
|
|
140
|
-
value: boolean;
|
|
140
|
+
value: boolean | null;
|
|
141
141
|
onChange: (value: boolean) => void;
|
|
142
142
|
inputClassName?: string;
|
|
143
143
|
onDesc?: string;
|
|
144
144
|
offDesc?: string;
|
|
145
145
|
disabled?: boolean;
|
|
146
146
|
}
|
|
147
|
-
interface ToggleSelectProps<I> {
|
|
147
|
+
interface ToggleSelectProps<I, Nullable extends boolean> {
|
|
148
148
|
className?: string;
|
|
149
149
|
labelClassName?: string;
|
|
150
150
|
label?: string;
|
|
@@ -156,11 +156,11 @@ interface ToggleSelectProps<I> {
|
|
|
156
156
|
value: I;
|
|
157
157
|
disabled?: boolean;
|
|
158
158
|
}[] | readonly I[] | I[] | EnumInstance<string, I>;
|
|
159
|
-
value: I;
|
|
160
|
-
nullable?:
|
|
159
|
+
value: I | null;
|
|
160
|
+
nullable?: Nullable;
|
|
161
161
|
disabled?: boolean;
|
|
162
162
|
validate?: (value: I) => boolean | string;
|
|
163
|
-
onChange: (value: I) => void;
|
|
163
|
+
onChange: (value: Nullable extends true ? I | null : I) => void;
|
|
164
164
|
btnClassName?: string;
|
|
165
165
|
}
|
|
166
166
|
interface MultiToggleSelectProps<I extends string | number | boolean> {
|
|
@@ -173,7 +173,7 @@ interface MultiToggleSelectProps<I extends string | number | boolean> {
|
|
|
173
173
|
value: I;
|
|
174
174
|
disabled?: boolean;
|
|
175
175
|
}[] | readonly I[] | I[];
|
|
176
|
-
value: I[];
|
|
176
|
+
value: I[] | null;
|
|
177
177
|
disabled?: boolean;
|
|
178
178
|
minlength?: number;
|
|
179
179
|
maxlength?: number;
|
|
@@ -185,7 +185,7 @@ interface TextListProps {
|
|
|
185
185
|
desc?: string;
|
|
186
186
|
labelClassName?: string;
|
|
187
187
|
className?: string;
|
|
188
|
-
value: string[];
|
|
188
|
+
value: string[] | null;
|
|
189
189
|
onChange: (value: string[]) => void;
|
|
190
190
|
inputClassName?: string;
|
|
191
191
|
placeholder?: string;
|
|
@@ -203,7 +203,7 @@ interface TagsProps {
|
|
|
203
203
|
desc?: string;
|
|
204
204
|
labelClassName?: string;
|
|
205
205
|
className?: string;
|
|
206
|
-
value: string[];
|
|
206
|
+
value: string[] | null;
|
|
207
207
|
onChange: (value: string[]) => void;
|
|
208
208
|
inputClassName?: string;
|
|
209
209
|
placeholder?: string;
|
|
@@ -386,7 +386,7 @@ interface ChildrenProps<T extends string, State, Input, Full, Light> {
|
|
|
386
386
|
disabled?: boolean;
|
|
387
387
|
nullable?: boolean;
|
|
388
388
|
initArgs?: any[];
|
|
389
|
-
value: Light[];
|
|
389
|
+
value: Light[] | null;
|
|
390
390
|
onChange: (value: Light[]) => void;
|
|
391
391
|
onSearch?: (text: string) => void;
|
|
392
392
|
slice: SliceMeta;
|
|
@@ -402,7 +402,7 @@ interface ChildrenIdProps<T extends string, State, Input, Full, Light> {
|
|
|
402
402
|
disabled?: boolean;
|
|
403
403
|
nullable?: boolean;
|
|
404
404
|
initArgs?: any[];
|
|
405
|
-
value: string[];
|
|
405
|
+
value: string[] | null;
|
|
406
406
|
slice: SliceMeta;
|
|
407
407
|
onChange: (value: string[]) => void;
|
|
408
408
|
onSearch?: (text: string) => void;
|
|
@@ -11,6 +11,7 @@ export interface ToggleSelectProps<I extends string | number | boolean | null> {
|
|
|
11
11
|
nullable: boolean;
|
|
12
12
|
validate: (value: I) => boolean | string;
|
|
13
13
|
onChange: (value: I, idx: number) => void;
|
|
14
|
+
onClear?: () => void;
|
|
14
15
|
disabled?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export interface MultiProps {
|
|
@@ -7,6 +7,7 @@ export * from "./AgentScope.d.ts";
|
|
|
7
7
|
export * from "./AgentSession.d.ts";
|
|
8
8
|
export * from "./Compaction.d.ts";
|
|
9
9
|
export * from "./httpRunner.d.ts";
|
|
10
|
+
export * from "./sharedContext.d.ts";
|
|
10
11
|
export * from "./surfaceContext.d.ts";
|
|
11
12
|
export * from "./ToolOutput.d.ts";
|
|
12
13
|
export * from "./Transcript.d.ts";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Context } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A React context interned on `globalThis`, for the reason `AgenticSurface.shared` is: an app does not get one
|
|
4
|
+
* copy of this package. An akan build inlines it into every client bundle that reaches it — four of them in this
|
|
5
|
+
* repo's own docs app — and a context is identified by object identity, so a `ScopeContext.Provider` rendered by
|
|
6
|
+
* `Agent.Zone` in one copy is invisible to the `st.tool` reading it from another.
|
|
7
|
+
*
|
|
8
|
+
* That failure is silent, which is what makes it worth a global: the read falls back to the context's default, so
|
|
9
|
+
* the tool registers at the root scope instead of the zone's, the zone session filters it out as belonging to a
|
|
10
|
+
* different view, and the model is handed the built-ins with nothing thrown and nothing logged.
|
|
11
|
+
*
|
|
12
|
+
* Every context this package owns goes through here. Adding one the plain way would work in the monorepo — one
|
|
13
|
+
* copy, one object — and fail only once bundled, which is exactly the bug this replaced.
|
|
14
|
+
*/
|
|
15
|
+
export declare const sharedContext: <T>(name: string, initial: T) => Context<T>;
|
package/ui/Agent/Context.tsx
CHANGED
|
@@ -9,16 +9,25 @@ interface ContextProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Assembles and shows
|
|
13
|
-
*
|
|
12
|
+
* Assembles and shows exactly what a turn would carry, on demand — the one preview of "what does the agent see on
|
|
13
|
+
* this screen" that no amount of reading the source answers.
|
|
14
|
+
*
|
|
15
|
+
* The tool list leads, by name only: a zone publishes its tools scope-prefixed, and instructions that name a tool
|
|
16
|
+
* without its prefix name a tool that does not exist. That is invisible in the source of either file and obvious
|
|
17
|
+
* here.
|
|
14
18
|
*/
|
|
15
19
|
export default function Context({ className }: ContextProps) {
|
|
16
20
|
const [shown, setShown] = useState("");
|
|
17
21
|
const assemble = () => {
|
|
18
22
|
try {
|
|
19
|
-
const { guides } = AgenticSurface.shared.snapshot();
|
|
23
|
+
const { guides, tools } = AgenticSurface.shared.snapshot();
|
|
20
24
|
const context = AgentContext.of().blocks(AgenticSurface.shared);
|
|
21
|
-
|
|
25
|
+
const assembled = {
|
|
26
|
+
tools: tools.map((tool) => tool.name),
|
|
27
|
+
...(guides.length ? { guides } : {}),
|
|
28
|
+
context,
|
|
29
|
+
};
|
|
30
|
+
setShown(JSON.stringify(assembled, null, 2));
|
|
22
31
|
} catch (thrown) {
|
|
23
32
|
setShown(thrown instanceof Error ? thrown.message : String(thrown));
|
|
24
33
|
}
|
package/ui/Agent/Zone.tsx
CHANGED
|
@@ -56,6 +56,10 @@ export interface ZoneProps {
|
|
|
56
56
|
* inside — hook tools, `st.use` subscriptions, guides — belongs to this zone's session *and* to the root agent:
|
|
57
57
|
* zones are views, never walls. An `Agent.Chat` mounted inside binds to this session automatically, so two zones
|
|
58
58
|
* on one screen run two conversations in parallel, each seeing only its own subtree.
|
|
59
|
+
*
|
|
60
|
+
* **Everything a zone publishes is named `<id>.<name>`.** Instructions that name a tool must carry the prefix —
|
|
61
|
+
* a bare name is a tool that does not exist, and the model calling it spends a turn on `Unknown tool`. Build the
|
|
62
|
+
* name from the id rather than writing it twice, and read `Agent.Context`'s Assemble to see the published list.
|
|
59
63
|
*/
|
|
60
64
|
export const Zone = ({
|
|
61
65
|
className,
|
package/ui/Field.tsx
CHANGED
|
@@ -86,7 +86,7 @@ interface ListProps<Item> {
|
|
|
86
86
|
label?: string;
|
|
87
87
|
desc?: string;
|
|
88
88
|
nullable?: boolean;
|
|
89
|
-
value: Item[];
|
|
89
|
+
value: Item[] | null;
|
|
90
90
|
onChange: (value: Item[]) => void;
|
|
91
91
|
onAdd: () => void;
|
|
92
92
|
renderItem: (item: Item, idx: number) => ReactNode;
|
|
@@ -105,11 +105,12 @@ const List = <Item,>({
|
|
|
105
105
|
const { l } = usePage();
|
|
106
106
|
const recipe = useUiRecipe("button") ?? buttonRecipe;
|
|
107
107
|
useFieldTool(onChange);
|
|
108
|
+
const items = value ?? [];
|
|
108
109
|
return (
|
|
109
110
|
<div {...agentAttrs(onChange)} className={cn("flex w-full flex-col", className)}>
|
|
110
111
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
111
112
|
<div className="mb-2 flex w-full flex-col gap-2 rounded-box border border-border p-2">
|
|
112
|
-
{
|
|
113
|
+
{items.map((item, idx) => (
|
|
113
114
|
<>
|
|
114
115
|
<div key={idx} className="flex h-full w-full items-center justify-between gap-2">
|
|
115
116
|
{renderItem(item, idx)}
|
|
@@ -120,7 +121,7 @@ const List = <Item,>({
|
|
|
120
121
|
"size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
|
|
121
122
|
)}
|
|
122
123
|
onClick={() => {
|
|
123
|
-
onChange(
|
|
124
|
+
onChange(items.filter((_, i) => i !== idx));
|
|
124
125
|
}}
|
|
125
126
|
>
|
|
126
127
|
<BiTrash />
|
|
@@ -351,7 +352,7 @@ interface SwitchProps {
|
|
|
351
352
|
desc?: string;
|
|
352
353
|
labelClassName?: string;
|
|
353
354
|
className?: string;
|
|
354
|
-
value: boolean;
|
|
355
|
+
value: boolean | null;
|
|
355
356
|
onChange: (value: boolean) => void;
|
|
356
357
|
inputClassName?: string;
|
|
357
358
|
onDesc?: string;
|
|
@@ -379,7 +380,7 @@ const Switch = ({
|
|
|
379
380
|
variant="accent"
|
|
380
381
|
disabled={disabled}
|
|
381
382
|
className={inputClassName}
|
|
382
|
-
checked={value}
|
|
383
|
+
checked={value ?? false}
|
|
383
384
|
onChange={(checked) => {
|
|
384
385
|
onChange(checked);
|
|
385
386
|
}}
|
|
@@ -391,7 +392,7 @@ const Switch = ({
|
|
|
391
392
|
};
|
|
392
393
|
Field.Switch = Switch;
|
|
393
394
|
|
|
394
|
-
interface ToggleSelectProps<I> {
|
|
395
|
+
interface ToggleSelectProps<I, Nullable extends boolean> {
|
|
395
396
|
className?: string;
|
|
396
397
|
labelClassName?: string;
|
|
397
398
|
label?: string;
|
|
@@ -399,14 +400,14 @@ interface ToggleSelectProps<I> {
|
|
|
399
400
|
model?: string;
|
|
400
401
|
field?: string;
|
|
401
402
|
items: { label: string; value: I; disabled?: boolean }[] | readonly I[] | I[] | EnumInstance<string, I>;
|
|
402
|
-
value: I;
|
|
403
|
-
nullable?:
|
|
403
|
+
value: I | null;
|
|
404
|
+
nullable?: Nullable;
|
|
404
405
|
disabled?: boolean;
|
|
405
406
|
validate?: (value: I) => boolean | string;
|
|
406
|
-
onChange: (value: I) => void;
|
|
407
|
+
onChange: (value: Nullable extends true ? I | null : I) => void;
|
|
407
408
|
btnClassName?: string;
|
|
408
409
|
}
|
|
409
|
-
const ToggleSelect = <I extends string | number | boolean | null>({
|
|
410
|
+
const ToggleSelect = <I extends string | number | boolean | null, Nullable extends boolean = false>({
|
|
410
411
|
className,
|
|
411
412
|
labelClassName,
|
|
412
413
|
label,
|
|
@@ -418,14 +419,15 @@ const ToggleSelect = <I extends string | number | boolean | null>({
|
|
|
418
419
|
nullable,
|
|
419
420
|
disabled,
|
|
420
421
|
btnClassName,
|
|
421
|
-
}: ToggleSelectProps<I>) => {
|
|
422
|
+
}: ToggleSelectProps<I, Nullable>) => {
|
|
422
423
|
useFieldTool(onChange, { disabled });
|
|
423
424
|
const { l } = usePage();
|
|
424
425
|
const isEnumValue = isEnum(items as EnumInstance<string, I>);
|
|
426
|
+
const change = onChange as (value: I | null) => void;
|
|
425
427
|
return (
|
|
426
428
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
427
429
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
428
|
-
<UtilToggleSelect
|
|
430
|
+
<UtilToggleSelect<I | null>
|
|
429
431
|
className="mt-2"
|
|
430
432
|
nullable={!!nullable}
|
|
431
433
|
btnClassName={btnClassName}
|
|
@@ -438,12 +440,15 @@ const ToggleSelect = <I extends string | number | boolean | null>({
|
|
|
438
440
|
: (items as { label: string; value: I; disabled?: boolean }[])
|
|
439
441
|
}
|
|
440
442
|
value={value}
|
|
441
|
-
onChange={(
|
|
442
|
-
|
|
443
|
+
onChange={(selected) => {
|
|
444
|
+
change(selected);
|
|
445
|
+
}}
|
|
446
|
+
onClear={() => {
|
|
447
|
+
change(null);
|
|
443
448
|
}}
|
|
444
449
|
disabled={disabled}
|
|
445
|
-
validate={(
|
|
446
|
-
return validate?.(
|
|
450
|
+
validate={(selected) => {
|
|
451
|
+
return selected === null ? true : (validate?.(selected) ?? true);
|
|
447
452
|
}}
|
|
448
453
|
/>
|
|
449
454
|
</div>
|
|
@@ -457,7 +462,7 @@ interface MultiToggleSelectProps<I extends string | number | boolean> {
|
|
|
457
462
|
label?: string;
|
|
458
463
|
desc?: string;
|
|
459
464
|
items: EnumInstance<string, I> | { label: string; value: I; disabled?: boolean }[] | readonly I[] | I[];
|
|
460
|
-
value: I[];
|
|
465
|
+
value: I[] | null;
|
|
461
466
|
disabled?: boolean;
|
|
462
467
|
minlength?: number;
|
|
463
468
|
maxlength?: number;
|
|
@@ -482,7 +487,7 @@ const MultiToggleSelect = <I extends string | number | boolean>({
|
|
|
482
487
|
const isEnumValue = isEnum(items as EnumInstance<string, I>);
|
|
483
488
|
return (
|
|
484
489
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
485
|
-
{label ? <Label className={labelClassName} nullable={
|
|
490
|
+
{label ? <Label className={labelClassName} nullable={!minlength} label={label} desc={desc} /> : null}
|
|
486
491
|
<UtilToggleSelect.Multi
|
|
487
492
|
nullable={!minlength}
|
|
488
493
|
items={
|
|
@@ -493,7 +498,7 @@ const MultiToggleSelect = <I extends string | number | boolean>({
|
|
|
493
498
|
})) as { label: string; value: string; disabled?: boolean }[])
|
|
494
499
|
: (items as { label: string; value: string; disabled?: boolean }[])
|
|
495
500
|
}
|
|
496
|
-
value={value as string[]}
|
|
501
|
+
value={(value ?? []) as string[]}
|
|
497
502
|
onChange={(values) => {
|
|
498
503
|
onChange(values as I[]);
|
|
499
504
|
}}
|
|
@@ -514,7 +519,7 @@ interface TextListProps {
|
|
|
514
519
|
desc?: string;
|
|
515
520
|
labelClassName?: string;
|
|
516
521
|
className?: string;
|
|
517
|
-
value: string[];
|
|
522
|
+
value: string[] | null;
|
|
518
523
|
onChange: (value: string[]) => void;
|
|
519
524
|
inputClassName?: string;
|
|
520
525
|
placeholder?: string;
|
|
@@ -548,6 +553,7 @@ const TextList = ({
|
|
|
548
553
|
useFieldTool(onChange, { transform, disabled, sortable: true });
|
|
549
554
|
const { l } = usePage();
|
|
550
555
|
const recipe = useUiRecipe("button") ?? buttonRecipe;
|
|
556
|
+
const texts = value ?? [];
|
|
551
557
|
return (
|
|
552
558
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
553
559
|
{label ? <Label className={labelClassName} nullable={!minlength} label={label} desc={desc} /> : null}
|
|
@@ -559,10 +565,10 @@ const TextList = ({
|
|
|
559
565
|
onChange(sorted);
|
|
560
566
|
}}
|
|
561
567
|
onRemove={(_, idx) => {
|
|
562
|
-
onChange(
|
|
568
|
+
onChange(texts.filter((_, i) => i !== idx));
|
|
563
569
|
}}
|
|
564
570
|
>
|
|
565
|
-
{
|
|
571
|
+
{texts.map((text, idx) => (
|
|
566
572
|
<DraggableList.Item key={idx} value={text}>
|
|
567
573
|
<div className="flex w-full items-center">
|
|
568
574
|
<DraggableList.Cursor>
|
|
@@ -573,7 +579,7 @@ const TextList = ({
|
|
|
573
579
|
value={text}
|
|
574
580
|
cacheKey={cache ? `${label}-${desc}-textList-[${idx}]` : undefined}
|
|
575
581
|
onChange={(text) => {
|
|
576
|
-
const newValue = [...
|
|
582
|
+
const newValue = [...texts];
|
|
577
583
|
newValue[idx] = transform(text);
|
|
578
584
|
onChange(newValue);
|
|
579
585
|
}}
|
|
@@ -593,7 +599,7 @@ const TextList = ({
|
|
|
593
599
|
"size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
|
|
594
600
|
)}
|
|
595
601
|
onClick={() => {
|
|
596
|
-
onChange(
|
|
602
|
+
onChange(texts.filter((_, i) => i !== idx));
|
|
597
603
|
}}
|
|
598
604
|
>
|
|
599
605
|
<BiTrash />
|
|
@@ -604,11 +610,11 @@ const TextList = ({
|
|
|
604
610
|
))}
|
|
605
611
|
</DraggableList>
|
|
606
612
|
<div className="my-5 h-[0.5px] bg-foreground/20" />
|
|
607
|
-
{
|
|
613
|
+
{texts.length <= maxTextlength ? (
|
|
608
614
|
<button
|
|
609
615
|
className={recipe({ variant: "outline" }, "w-full")}
|
|
610
616
|
onClick={() => {
|
|
611
|
-
onChange([...
|
|
617
|
+
onChange([...texts, ""]);
|
|
612
618
|
}}
|
|
613
619
|
>
|
|
614
620
|
+ New
|
|
@@ -625,7 +631,7 @@ interface TagsProps {
|
|
|
625
631
|
desc?: string;
|
|
626
632
|
labelClassName?: string;
|
|
627
633
|
className?: string;
|
|
628
|
-
value: string[];
|
|
634
|
+
value: string[] | null;
|
|
629
635
|
onChange: (value: string[]) => void;
|
|
630
636
|
inputClassName?: string;
|
|
631
637
|
placeholder?: string;
|
|
@@ -658,11 +664,12 @@ const Tags = ({
|
|
|
658
664
|
useFieldTool(onChange, { transform, disabled });
|
|
659
665
|
const { l } = usePage();
|
|
660
666
|
const badge = useUiRecipe("badge") ?? badgeRecipe;
|
|
667
|
+
const tagList = value ?? [];
|
|
661
668
|
const [inputVisible, setInputVisible] = useState(false);
|
|
662
669
|
const [tag, setTag] = useState("");
|
|
663
670
|
const addTag = () => {
|
|
664
671
|
if (!tag.length) return;
|
|
665
|
-
onChange([...
|
|
672
|
+
onChange([...tagList, tag]);
|
|
666
673
|
setInputVisible(false);
|
|
667
674
|
setTag("");
|
|
668
675
|
};
|
|
@@ -671,14 +678,14 @@ const Tags = ({
|
|
|
671
678
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
672
679
|
{label ? <Label className={labelClassName} nullable={!minlength} label={label} desc={desc} /> : null}
|
|
673
680
|
<div className="flex w-full flex-wrap items-center gap-1 rounded-box border border-border p-2">
|
|
674
|
-
{
|
|
681
|
+
{tagList.map((val, idx) => (
|
|
675
682
|
<span className={badge({ variant: "outline" }, "items-center")} key={idx}>
|
|
676
683
|
<div className="text-xs italic">#</div>
|
|
677
684
|
{val}
|
|
678
685
|
<BiX
|
|
679
686
|
className="ml-1 cursor-pointer opacity-50 duration-200 hover:opacity-100"
|
|
680
687
|
onClick={() => {
|
|
681
|
-
if (!disabled) onChange(
|
|
688
|
+
if (!disabled) onChange(tagList.filter((v, i) => i !== idx));
|
|
682
689
|
}}
|
|
683
690
|
/>
|
|
684
691
|
</span>
|
|
@@ -1455,7 +1462,7 @@ interface ChildrenProps<T extends string, State, Input, Full, Light> {
|
|
|
1455
1462
|
disabled?: boolean;
|
|
1456
1463
|
nullable?: boolean;
|
|
1457
1464
|
initArgs?: any[];
|
|
1458
|
-
value: Light[];
|
|
1465
|
+
value: Light[] | null;
|
|
1459
1466
|
onChange: (value: Light[]) => void;
|
|
1460
1467
|
onSearch?: (text: string) => void;
|
|
1461
1468
|
slice: SliceMeta;
|
|
@@ -1515,7 +1522,7 @@ const Children = <T extends string, State, Input, Full extends { id: string }, L
|
|
|
1515
1522
|
labelClassName={labelClassName}
|
|
1516
1523
|
selectClassName={selectClassName}
|
|
1517
1524
|
multiple
|
|
1518
|
-
value={value.map((model) => model.id)}
|
|
1525
|
+
value={(value ?? []).map((model) => model.id)}
|
|
1519
1526
|
options={modelList.map((model) => {
|
|
1520
1527
|
const label = renderOption(model);
|
|
1521
1528
|
return { label: typeof label === "string" ? label : model.id, value: model.id };
|
|
@@ -1554,7 +1561,7 @@ interface ChildrenIdProps<T extends string, State, Input, Full, Light> {
|
|
|
1554
1561
|
disabled?: boolean;
|
|
1555
1562
|
nullable?: boolean;
|
|
1556
1563
|
initArgs?: any[];
|
|
1557
|
-
value: string[];
|
|
1564
|
+
value: string[] | null;
|
|
1558
1565
|
slice: SliceMeta;
|
|
1559
1566
|
onChange: (value: string[]) => void;
|
|
1560
1567
|
onSearch?: (text: string) => void;
|
|
@@ -1607,7 +1614,7 @@ const ChildrenId = <T extends string, State, Input, Full extends { id: string },
|
|
|
1607
1614
|
labelClassName={labelClassName}
|
|
1608
1615
|
multiple
|
|
1609
1616
|
|
|
1610
|
-
value={value}
|
|
1617
|
+
value={value ?? []}
|
|
1611
1618
|
options={modelList.map((model) => {
|
|
1612
1619
|
const label = renderOption(model);
|
|
1613
1620
|
return { label: typeof label === "string" ? label : model.id, value: model.id };
|
package/ui/ToggleSelect.tsx
CHANGED
|
@@ -16,6 +16,7 @@ export interface ToggleSelectProps<I extends string | number | boolean | null> {
|
|
|
16
16
|
nullable: boolean;
|
|
17
17
|
validate: (value: I) => boolean | string;
|
|
18
18
|
onChange: (value: I, idx: number) => void;
|
|
19
|
+
onClear?: () => void;
|
|
19
20
|
disabled?: boolean;
|
|
20
21
|
}
|
|
21
22
|
const DefaultToggleSelect = <I extends string | number | boolean | null>({
|
|
@@ -26,6 +27,7 @@ const DefaultToggleSelect = <I extends string | number | boolean | null>({
|
|
|
26
27
|
validate,
|
|
27
28
|
value,
|
|
28
29
|
onChange,
|
|
30
|
+
onClear,
|
|
29
31
|
disabled,
|
|
30
32
|
}: ToggleSelectProps<I>) => {
|
|
31
33
|
const { l } = usePage();
|
|
@@ -61,7 +63,8 @@ const DefaultToggleSelect = <I extends string | number | boolean | null>({
|
|
|
61
63
|
disabled={isDisabled}
|
|
62
64
|
className={cn(toggleBtn, isSelected && selectedCls, isDisabled && "cursor-not-allowed", btnClassName)}
|
|
63
65
|
onClick={() => {
|
|
64
|
-
|
|
66
|
+
if (nullable && isSelected) onClear?.();
|
|
67
|
+
else onChange(option.value, idx);
|
|
65
68
|
}}
|
|
66
69
|
>
|
|
67
70
|
{option.label}
|
|
@@ -8,6 +8,7 @@ export * from "./AgentScope";
|
|
|
8
8
|
export * from "./AgentSession";
|
|
9
9
|
export * from "./Compaction";
|
|
10
10
|
export * from "./httpRunner";
|
|
11
|
+
export * from "./sharedContext";
|
|
11
12
|
export * from "./surfaceContext";
|
|
12
13
|
export * from "./ToolOutput";
|
|
13
14
|
export * from "./Transcript";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type Context, createContext } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A React context interned on `globalThis`, for the reason `AgenticSurface.shared` is: an app does not get one
|
|
6
|
+
* copy of this package. An akan build inlines it into every client bundle that reaches it — four of them in this
|
|
7
|
+
* repo's own docs app — and a context is identified by object identity, so a `ScopeContext.Provider` rendered by
|
|
8
|
+
* `Agent.Zone` in one copy is invisible to the `st.tool` reading it from another.
|
|
9
|
+
*
|
|
10
|
+
* That failure is silent, which is what makes it worth a global: the read falls back to the context's default, so
|
|
11
|
+
* the tool registers at the root scope instead of the zone's, the zone session filters it out as belonging to a
|
|
12
|
+
* different view, and the model is handed the built-ins with nothing thrown and nothing logged.
|
|
13
|
+
*
|
|
14
|
+
* Every context this package owns goes through here. Adding one the plain way would work in the monorepo — one
|
|
15
|
+
* copy, one object — and fail only once bundled, which is exactly the bug this replaced.
|
|
16
|
+
*/
|
|
17
|
+
export const sharedContext = <T>(name: string, initial: T): Context<T> => {
|
|
18
|
+
const key = Symbol.for(`useAgentic.context.${name}`);
|
|
19
|
+
const holder = globalThis as typeof globalThis & { [slot: symbol]: Context<T> | undefined };
|
|
20
|
+
const existing = holder[key];
|
|
21
|
+
if (existing) return existing;
|
|
22
|
+
const created = createContext(initial);
|
|
23
|
+
holder[key] = created;
|
|
24
|
+
return created;
|
|
25
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext } from "react";
|
|
3
3
|
import { AgenticSurface } from "./AgenticSurface";
|
|
4
|
+
import { sharedContext } from "./sharedContext";
|
|
4
5
|
|
|
5
|
-
export const SurfaceContext =
|
|
6
|
-
export const ScopeContext =
|
|
6
|
+
export const SurfaceContext = sharedContext<AgenticSurface | null>("surface", null);
|
|
7
|
+
export const ScopeContext = sharedContext<string[]>("scope", []);
|
|
7
8
|
|
|
8
9
|
export const useSurface = () => useContext(SurfaceContext) ?? AgenticSurface.shared;
|
|
9
10
|
export const useScopePath = () => useContext(ScopeContext);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext, useSyncExternalStore } from "react";
|
|
3
3
|
import type { AgentSession } from "./AgentSession";
|
|
4
|
+
import { sharedContext } from "./sharedContext";
|
|
4
5
|
|
|
5
|
-
export const SessionContext =
|
|
6
|
+
export const SessionContext = sharedContext<AgentSession | null>("session", null);
|
|
6
7
|
|
|
7
8
|
/** The enclosing session, re-rendering on every session change. `send`/`abort` are safe to destructure. */
|
|
8
9
|
export const useAgent = (): AgentSession => {
|