@temporalio/agent-harness-svelte 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/session.svelte.d.ts +69 -0
- package/dist/session.svelte.js +161 -0
- package/dist/state.svelte.d.ts +11 -0
- package/dist/state.svelte.js +21 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Temporal Technologies Inc. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @temporalio/agent-harness-svelte
|
|
2
|
+
|
|
3
|
+
A Svelte 5 binding for `@temporalio/agent-harness-client`: `AgentSession`, a reactive handle on one
|
|
4
|
+
harness agent session.
|
|
5
|
+
|
|
6
|
+
```svelte
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { AgentSession } from "@temporalio/agent-harness-svelte";
|
|
9
|
+
import type { TicTacToeAgent } from "./client_sdk/TicTacToeAgent"; // from harness-codegen
|
|
10
|
+
|
|
11
|
+
let { sessionId } = $props();
|
|
12
|
+
const agent = new AgentSession<TicTacToeAgent>({ get sessionId() { return sessionId; } });
|
|
13
|
+
const board = $derived(agent.states.board);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
{#if board}
|
|
17
|
+
{#each board.cells as cell, i (i)}
|
|
18
|
+
<button onclick={() => agent.sendMessage("play", { cell: i + 1 })}>{cell ?? ""}</button>
|
|
19
|
+
{/each}
|
|
20
|
+
{/if}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **Reading opens the connection.** The first effect or markup to read any field opens the
|
|
24
|
+
session's stream; it closes once nothing reads it, so unmounting the last view releases it.
|
|
25
|
+
- **Getter options follow changes.** A `get sessionId()` switches the session when its value does.
|
|
26
|
+
- **One connection per session.** Call `createHarnessContext()` in a parent component and every
|
|
27
|
+
`AgentSession` below it with the same session id shares one connection and one state.
|
|
28
|
+
- **The whole tree.** `messages`, `states` and `agentStatus` are the root agent's; `agents` and
|
|
29
|
+
`agent(id)` reach every subagent, and a `subagent` part's `messageId` names the child message
|
|
30
|
+
it ran. `pendingApprovals` and `pendingCallbacks` collect waiting tool calls from all of them.
|
|
31
|
+
|
|
32
|
+
## Development
|
|
33
|
+
|
|
34
|
+
This package links `../client` and resolves it through that package's build:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
(cd ../client && npm ci && npm run build)
|
|
38
|
+
npm ci
|
|
39
|
+
npm run check && npm test && npm run build
|
|
40
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AgentSessionCore, type AgentSchema, type AgentSessionOptions as CoreOptions, type AgentSseFrame, type AgentView, type ConnectionStatus, type HandlerInput, type HandlerName, type HarnessMessage, type SubmitMessageResponse, type ToolApprovalDecision, type UntypedAgent, type WaitingToolCall } from "@temporalio/agent-harness-client";
|
|
2
|
+
import { SvelteSessionState } from "./state.svelte.js";
|
|
3
|
+
export interface AgentSessionOptions<A extends AgentSchema> extends Omit<CoreOptions<A>, "sessionId"> {
|
|
4
|
+
/** The session to follow. Pass a getter (`get sessionId() { return id; }`) to switch
|
|
5
|
+
* sessions when `id` changes. */
|
|
6
|
+
readonly sessionId: string;
|
|
7
|
+
}
|
|
8
|
+
/** One session's core and state, shared by every AgentSession that follows it. */
|
|
9
|
+
declare class SessionEntry<A extends AgentSchema> {
|
|
10
|
+
#private;
|
|
11
|
+
readonly state: SvelteSessionState<A>;
|
|
12
|
+
readonly core: AgentSessionCore<A>;
|
|
13
|
+
constructor(options: CoreOptions<A>);
|
|
14
|
+
acquire(): void;
|
|
15
|
+
release(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sessions by id, so that components following the same session share one connection and one
|
|
19
|
+
* state. Put one in context with `createHarnessContext()`; an AgentSession constructed outside
|
|
20
|
+
* any such context gets its own.
|
|
21
|
+
*
|
|
22
|
+
* The first AgentSession to follow an id decides that session's options (`callbackTools`,
|
|
23
|
+
* transport and so on); later ones share what it set up.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SessionStore {
|
|
26
|
+
#private;
|
|
27
|
+
entry<A extends AgentSchema>(options: CoreOptions<A>): SessionEntry<A>;
|
|
28
|
+
}
|
|
29
|
+
/** Share sessions below this component: every AgentSession constructed in its subtree for the
|
|
30
|
+
* same session id uses one connection. Call it during component initialisation. */
|
|
31
|
+
export declare function createHarnessContext(store?: SessionStore): SessionStore;
|
|
32
|
+
export declare class AgentSession<A extends AgentSchema = UntypedAgent> {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(options: AgentSessionOptions<A>, store?: SessionStore);
|
|
35
|
+
get sessionId(): string;
|
|
36
|
+
get connection(): ConnectionStatus;
|
|
37
|
+
get error(): Error | undefined;
|
|
38
|
+
/** The root agent's messages. */
|
|
39
|
+
get messages(): readonly HarnessMessage<A>[];
|
|
40
|
+
/** The root agent's observable state, by state id; each is undefined until its snapshot. */
|
|
41
|
+
get states(): Readonly<Partial<A["states"]>>;
|
|
42
|
+
/** Whether the root agent has a turn open. */
|
|
43
|
+
get agentStatus(): "idle" | "busy";
|
|
44
|
+
/** Messages sent from here that the server has not admitted yet, and failed sends. */
|
|
45
|
+
get pending(): readonly HarnessMessage<A>[];
|
|
46
|
+
/** Every agent in the session's tree — the root and its subagents at any depth — by id. */
|
|
47
|
+
get agents(): Readonly<Record<string, AgentView>>;
|
|
48
|
+
get rootAgentId(): string | null;
|
|
49
|
+
/** Every frame received, from every agent. */
|
|
50
|
+
get frames(): readonly AgentSseFrame[];
|
|
51
|
+
/** Gated tool calls waiting on a person, anywhere in the tree. */
|
|
52
|
+
get pendingApprovals(): readonly WaitingToolCall[];
|
|
53
|
+
/** Callback tool calls waiting on a client, anywhere in the tree, other than the ones the
|
|
54
|
+
* session's `callbackTools` answer. */
|
|
55
|
+
get pendingCallbacks(): readonly WaitingToolCall[];
|
|
56
|
+
/** Any agent in the tree by id — a subagent part's `subagentId`, say — typed as `B` when its
|
|
57
|
+
* generated type is given. */
|
|
58
|
+
agent<B extends AgentSchema = UntypedAgent>(agentId: string): AgentView<B> | undefined;
|
|
59
|
+
sendMessage<H extends HandlerName<A>>(handler: H, payload: HandlerInput<A, H>): Promise<SubmitMessageResponse | null>;
|
|
60
|
+
respondToApproval(toolId: string, decision: ToolApprovalDecision): Promise<boolean>;
|
|
61
|
+
provideToolOutput(toolId: string, result: unknown): Promise<boolean>;
|
|
62
|
+
provideToolError(toolId: string, error: string): Promise<boolean>;
|
|
63
|
+
/** Stop the agent: it finishes its turn loop and auto-denies anything pending. */
|
|
64
|
+
closeSession(): Promise<boolean>;
|
|
65
|
+
/** Drop a failed send from `pending`. */
|
|
66
|
+
dismiss(localId: string): void;
|
|
67
|
+
clearError(): void;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// AgentSession: a reactive handle on one agent session. Construct it in a component, read its
|
|
2
|
+
// fields in markup or effects, and call its actions. The connection is open exactly while
|
|
3
|
+
// something reads it: the first effect to read any field opens it, and it closes when the last
|
|
4
|
+
// such effect is torn down, so unmounting the last view of a session releases its stream.
|
|
5
|
+
import { getContext, hasContext, setContext } from "svelte";
|
|
6
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
7
|
+
import { AgentSessionCore, rootView, waitingCalls } from "@temporalio/agent-harness-client";
|
|
8
|
+
import { SvelteSessionState } from "./state.svelte.js";
|
|
9
|
+
/** One session's core and state, shared by every AgentSession that follows it. */
|
|
10
|
+
class SessionEntry {
|
|
11
|
+
state = new SvelteSessionState();
|
|
12
|
+
core;
|
|
13
|
+
#readers = 0;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.core = new AgentSessionCore(options, this.state);
|
|
16
|
+
}
|
|
17
|
+
acquire() {
|
|
18
|
+
if (this.#readers++ === 0)
|
|
19
|
+
this.core.start();
|
|
20
|
+
}
|
|
21
|
+
release() {
|
|
22
|
+
if (--this.#readers === 0)
|
|
23
|
+
this.core.stop();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sessions by id, so that components following the same session share one connection and one
|
|
28
|
+
* state. Put one in context with `createHarnessContext()`; an AgentSession constructed outside
|
|
29
|
+
* any such context gets its own.
|
|
30
|
+
*
|
|
31
|
+
* The first AgentSession to follow an id decides that session's options (`callbackTools`,
|
|
32
|
+
* transport and so on); later ones share what it set up.
|
|
33
|
+
*/
|
|
34
|
+
export class SessionStore {
|
|
35
|
+
#entries = new Map();
|
|
36
|
+
entry(options) {
|
|
37
|
+
let entry = this.#entries.get(options.sessionId);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
entry = new SessionEntry(options);
|
|
40
|
+
this.#entries.set(options.sessionId, entry);
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const STORE = Symbol("temporal-agent-harness sessions");
|
|
46
|
+
/** Share sessions below this component: every AgentSession constructed in its subtree for the
|
|
47
|
+
* same session id uses one connection. Call it during component initialisation. */
|
|
48
|
+
export function createHarnessContext(store = new SessionStore()) {
|
|
49
|
+
return setContext(STORE, store);
|
|
50
|
+
}
|
|
51
|
+
function contextStore() {
|
|
52
|
+
// Outside component initialisation there is no context to read, and asking throws.
|
|
53
|
+
try {
|
|
54
|
+
return hasContext(STORE) ? getContext(STORE) : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class AgentSession {
|
|
61
|
+
#options;
|
|
62
|
+
#store;
|
|
63
|
+
#entry = $derived.by(() => this.#resolve());
|
|
64
|
+
#subscribe;
|
|
65
|
+
#waiting = $derived.by(() => waitingCalls(this.#state().agents, (toolName) => this.#entry.core.handlesCallback(toolName)));
|
|
66
|
+
constructor(options, store) {
|
|
67
|
+
this.#options = options;
|
|
68
|
+
this.#store = store ?? contextStore() ?? new SessionStore();
|
|
69
|
+
this.#subscribe = createSubscriber(() =>
|
|
70
|
+
// Hold the session for as long as anything reads it. The inner effect follows
|
|
71
|
+
// `sessionId`, so switching sessions moves the hold from the old one to the new one.
|
|
72
|
+
$effect.root(() => {
|
|
73
|
+
$effect(() => {
|
|
74
|
+
const entry = this.#entry;
|
|
75
|
+
entry.acquire();
|
|
76
|
+
return () => entry.release();
|
|
77
|
+
});
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
get sessionId() {
|
|
81
|
+
return this.#options.sessionId;
|
|
82
|
+
}
|
|
83
|
+
get connection() {
|
|
84
|
+
return this.#state().connection;
|
|
85
|
+
}
|
|
86
|
+
get error() {
|
|
87
|
+
return this.#state().error;
|
|
88
|
+
}
|
|
89
|
+
/** The root agent's messages. */
|
|
90
|
+
get messages() {
|
|
91
|
+
return rootView(this.#state()).messages;
|
|
92
|
+
}
|
|
93
|
+
/** The root agent's observable state, by state id; each is undefined until its snapshot. */
|
|
94
|
+
get states() {
|
|
95
|
+
return rootView(this.#state()).states;
|
|
96
|
+
}
|
|
97
|
+
/** Whether the root agent has a turn open. */
|
|
98
|
+
get agentStatus() {
|
|
99
|
+
return rootView(this.#state()).agentStatus;
|
|
100
|
+
}
|
|
101
|
+
/** Messages sent from here that the server has not admitted yet, and failed sends. */
|
|
102
|
+
get pending() {
|
|
103
|
+
return this.#state().pending;
|
|
104
|
+
}
|
|
105
|
+
/** Every agent in the session's tree — the root and its subagents at any depth — by id. */
|
|
106
|
+
get agents() {
|
|
107
|
+
return this.#state().agents;
|
|
108
|
+
}
|
|
109
|
+
get rootAgentId() {
|
|
110
|
+
return this.#state().rootAgentId;
|
|
111
|
+
}
|
|
112
|
+
/** Every frame received, from every agent. */
|
|
113
|
+
get frames() {
|
|
114
|
+
return this.#state().frames;
|
|
115
|
+
}
|
|
116
|
+
/** Gated tool calls waiting on a person, anywhere in the tree. */
|
|
117
|
+
get pendingApprovals() {
|
|
118
|
+
return this.#waiting.approvals;
|
|
119
|
+
}
|
|
120
|
+
/** Callback tool calls waiting on a client, anywhere in the tree, other than the ones the
|
|
121
|
+
* session's `callbackTools` answer. */
|
|
122
|
+
get pendingCallbacks() {
|
|
123
|
+
return this.#waiting.callbacks;
|
|
124
|
+
}
|
|
125
|
+
/** Any agent in the tree by id — a subagent part's `subagentId`, say — typed as `B` when its
|
|
126
|
+
* generated type is given. */
|
|
127
|
+
agent(agentId) {
|
|
128
|
+
return this.#state().agents[agentId];
|
|
129
|
+
}
|
|
130
|
+
sendMessage(handler, payload) {
|
|
131
|
+
return this.#entry.core.sendMessage(handler, payload);
|
|
132
|
+
}
|
|
133
|
+
respondToApproval(toolId, decision) {
|
|
134
|
+
return this.#entry.core.respondToApproval(toolId, decision);
|
|
135
|
+
}
|
|
136
|
+
provideToolOutput(toolId, result) {
|
|
137
|
+
return this.#entry.core.provideToolOutput(toolId, result);
|
|
138
|
+
}
|
|
139
|
+
provideToolError(toolId, error) {
|
|
140
|
+
return this.#entry.core.provideToolError(toolId, error);
|
|
141
|
+
}
|
|
142
|
+
/** Stop the agent: it finishes its turn loop and auto-denies anything pending. */
|
|
143
|
+
closeSession() {
|
|
144
|
+
return this.#entry.core.closeSession();
|
|
145
|
+
}
|
|
146
|
+
/** Drop a failed send from `pending`. */
|
|
147
|
+
dismiss(localId) {
|
|
148
|
+
this.#entry.core.dismiss(localId);
|
|
149
|
+
}
|
|
150
|
+
clearError() {
|
|
151
|
+
this.#entry.core.clearError();
|
|
152
|
+
}
|
|
153
|
+
#state() {
|
|
154
|
+
this.#subscribe();
|
|
155
|
+
return this.#entry.state;
|
|
156
|
+
}
|
|
157
|
+
#resolve() {
|
|
158
|
+
const { sessionId, ...rest } = this.#options;
|
|
159
|
+
return this.#store.entry({ ...rest, sessionId });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentSchema, type AgentSseFrame, type AgentUpdate, type AgentView, type ConnectionStatus, type HarnessMessage, type SessionState, type UntypedAgent } from "@temporalio/agent-harness-client";
|
|
2
|
+
export declare class SvelteSessionState<A extends AgentSchema = UntypedAgent> implements SessionState<A> {
|
|
3
|
+
connection: ConnectionStatus;
|
|
4
|
+
error: Error | undefined;
|
|
5
|
+
rootAgentId: string | null;
|
|
6
|
+
agents: Readonly<Record<string, AgentView<UntypedAgent>>>;
|
|
7
|
+
pending: readonly HarnessMessage<A>[];
|
|
8
|
+
frames: readonly AgentSseFrame[];
|
|
9
|
+
updateAgents(updates: ReadonlyArray<readonly [string, AgentUpdate]>): void;
|
|
10
|
+
appendFrames(frames: readonly AgentSseFrame[]): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The SessionState the core writes into, as Svelte state. `$state.raw` rather than deep
|
|
2
|
+
// `$state`: the core always hands over whole new objects, so deep proxies would buy nothing and
|
|
3
|
+
// make replaying a long session expensive.
|
|
4
|
+
import { applyAgentUpdate } from "@temporalio/agent-harness-client";
|
|
5
|
+
export class SvelteSessionState {
|
|
6
|
+
connection = $state("stopped");
|
|
7
|
+
error = $state.raw(undefined);
|
|
8
|
+
rootAgentId = $state(null);
|
|
9
|
+
agents = $state.raw({});
|
|
10
|
+
pending = $state.raw([]);
|
|
11
|
+
frames = $state.raw([]);
|
|
12
|
+
updateAgents(updates) {
|
|
13
|
+
const next = { ...this.agents };
|
|
14
|
+
for (const [id, update] of updates)
|
|
15
|
+
next[id] = applyAgentUpdate(next[id], id, update);
|
|
16
|
+
this.agents = next;
|
|
17
|
+
}
|
|
18
|
+
appendFrames(frames) {
|
|
19
|
+
this.frames = this.frames.concat(frames);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@temporalio/agent-harness-svelte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Svelte 5 binding for @temporalio/agent-harness-client: a reactive AgentSession over a temporal-agent-harness agent's event stream.",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"svelte": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "svelte-package",
|
|
23
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"svelte": "^5.7.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@temporalio/agent-harness-client": "^0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@sveltejs/package": "^2.5.8",
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
35
|
+
"@types/node": "^22.20.4",
|
|
36
|
+
"happy-dom": "^20.14.5",
|
|
37
|
+
"svelte": "^5.57.1",
|
|
38
|
+
"svelte-check": "^4.7.6",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vite": "^8.3.0",
|
|
41
|
+
"vitest": "^5.0.1"
|
|
42
|
+
}
|
|
43
|
+
}
|