@pi-oxide/pi-host-web 0.3.1 → 0.4.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/package.json +35 -4
- package/pi_host_web.d.ts +97 -202
- package/pi_host_web.js +218 -109
- package/pi_host_web_bg.wasm +0 -0
- package/sdk/agent.ts +274 -0
- package/sdk/artifacts.ts +35 -0
- package/sdk/context.ts +4 -0
- package/sdk/errors.ts +24 -0
- package/sdk/events.ts +52 -0
- package/sdk/index.d.ts +17 -43
- package/sdk/index.js +86 -220
- package/sdk/index.ts +53 -0
- package/sdk/init.ts +58 -0
- package/sdk/internal/engine.ts +614 -0
- package/sdk/internal/events.ts +241 -0
- package/sdk/internal/providers/anthropic.ts +440 -0
- package/sdk/internal/providers/openai.ts +177 -0
- package/sdk/internal/providers/types.ts +64 -0
- package/sdk/internal/stores/indexedDb.ts +24 -0
- package/sdk/internal/stores/persistence.ts +71 -0
- package/sdk/internal/tools/artifact.ts +24 -0
- package/sdk/internal/tools/browser.ts +449 -0
- package/sdk/internal/tools/browserRuntime.ts +48 -0
- package/sdk/internal/tools/liveRuntime.ts +151 -0
- package/sdk/internal/tools/registry.ts +174 -0
- package/sdk/internal/tools/service.ts +157 -0
- package/sdk/model.ts +35 -0
- package/sdk/react/index.ts +1 -0
- package/sdk/react/useAgent.ts +334 -0
- package/sdk/snapshot.ts +25 -0
- package/sdk/stores.ts +72 -0
- package/sdk/tools.ts +47 -0
- package/sdk/types.ts +252 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// openaiCompatible() and openai() factories — OpenAI-compatible provider adapters.
|
|
2
|
+
// openai() is a thin wrapper with baseUrl: "https://api.openai.com" (no /v1).
|
|
3
|
+
// openaiCompatible() appends /v1/chat/completions to the baseUrl.
|
|
4
|
+
// Correct message format: content: string | null, tool_calls: [...]
|
|
5
|
+
// Response parsing with tool_calls into AgentContentBlock[].
|
|
6
|
+
|
|
7
|
+
import type { AgentModel, ModelRequest, ModelResponse, AgentContentBlock } from "../../types.ts";
|
|
8
|
+
import { createAgentError } from "../../errors.ts";
|
|
9
|
+
|
|
10
|
+
export function openaiCompatible(config: {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
model: string;
|
|
14
|
+
maxTokens?: number;
|
|
15
|
+
}): AgentModel {
|
|
16
|
+
return {
|
|
17
|
+
id: config.model,
|
|
18
|
+
contextWindow: 128000,
|
|
19
|
+
maxTokens: config.maxTokens ?? 4096,
|
|
20
|
+
capabilities: {
|
|
21
|
+
vision: config.model.includes("vision") || config.model.includes("gpt-4o"),
|
|
22
|
+
jsonMode: true,
|
|
23
|
+
functionCalling: true,
|
|
24
|
+
streaming: true,
|
|
25
|
+
},
|
|
26
|
+
async generate(request: ModelRequest): Promise<ModelResponse> {
|
|
27
|
+
// Convert AgentMessage[] -> OpenAI Chat Completions message format
|
|
28
|
+
const messages = request.messages.map((msg) => {
|
|
29
|
+
switch (msg.role) {
|
|
30
|
+
case "user": {
|
|
31
|
+
const text = msg.content
|
|
32
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
33
|
+
.map((c) => c.text)
|
|
34
|
+
.join("\n");
|
|
35
|
+
return { role: "user" as const, content: text };
|
|
36
|
+
}
|
|
37
|
+
case "assistant": {
|
|
38
|
+
const textBlocks = msg.content
|
|
39
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
40
|
+
.map((c) => c.text)
|
|
41
|
+
.join("");
|
|
42
|
+
const toolCalls = msg.content
|
|
43
|
+
.filter((c): c is { type: "tool_call"; id: string; name: string; arguments: unknown } => c.type === "tool_call")
|
|
44
|
+
.map((c) => ({
|
|
45
|
+
id: c.id,
|
|
46
|
+
type: "function" as const,
|
|
47
|
+
function: {
|
|
48
|
+
name: c.name,
|
|
49
|
+
arguments: JSON.stringify(c.arguments),
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
role: "assistant" as const,
|
|
54
|
+
content: textBlocks || null,
|
|
55
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case "tool_result": {
|
|
59
|
+
const text = msg.content
|
|
60
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
61
|
+
.map((c) => c.text)
|
|
62
|
+
.join("\n");
|
|
63
|
+
return {
|
|
64
|
+
role: "tool" as const,
|
|
65
|
+
tool_call_id: msg.tool_call_id ?? "",
|
|
66
|
+
content: text,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Convert AgentToolDefinition[] -> OpenAI functions format
|
|
73
|
+
const tools = request.tools.map((t) => ({
|
|
74
|
+
type: "function" as const,
|
|
75
|
+
function: {
|
|
76
|
+
name: t.name,
|
|
77
|
+
description: t.description,
|
|
78
|
+
parameters: t.inputSchema as object,
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const body = {
|
|
83
|
+
model: config.model,
|
|
84
|
+
messages,
|
|
85
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
86
|
+
max_tokens: config.maxTokens,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const resp = await fetch(`${config.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
signal: request.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!resp.ok) {
|
|
101
|
+
const status = resp.status;
|
|
102
|
+
const text = await resp.text();
|
|
103
|
+
throw createAgentError(
|
|
104
|
+
status === 401 ? "model_auth_failed" :
|
|
105
|
+
status === 429 ? "model_rate_limited" :
|
|
106
|
+
"model_unavailable",
|
|
107
|
+
`HTTP ${status}: ${text}`,
|
|
108
|
+
{ recoverable: status === 429 },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = await resp.json();
|
|
113
|
+
const choice = data.choices?.[0];
|
|
114
|
+
const message = choice?.message;
|
|
115
|
+
|
|
116
|
+
// Parse content and tool_calls from OpenAI response
|
|
117
|
+
const content: AgentContentBlock[] = [];
|
|
118
|
+
|
|
119
|
+
if (message?.content && typeof message.content === "string") {
|
|
120
|
+
content.push({ type: "text", text: message.content });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (message?.tool_calls && Array.isArray(message.tool_calls)) {
|
|
124
|
+
for (const tc of message.tool_calls) {
|
|
125
|
+
if (tc.type === "function") {
|
|
126
|
+
content.push({
|
|
127
|
+
type: "tool_call",
|
|
128
|
+
id: tc.id ?? "",
|
|
129
|
+
name: tc.function?.name ?? "",
|
|
130
|
+
arguments: (() => {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(tc.function?.arguments ?? "{}");
|
|
133
|
+
} catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
})(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
content,
|
|
144
|
+
stopReason: choice?.finish_reason === "tool_calls" ? "tool_call" :
|
|
145
|
+
choice?.finish_reason === "stop" ? "end" :
|
|
146
|
+
choice?.finish_reason === "length" ? "length" : "error",
|
|
147
|
+
usage: data.usage ? {
|
|
148
|
+
input: data.usage.prompt_tokens,
|
|
149
|
+
output: data.usage.completion_tokens,
|
|
150
|
+
cache_read: 0,
|
|
151
|
+
cache_write: 0,
|
|
152
|
+
total_tokens: data.usage.total_tokens,
|
|
153
|
+
} : undefined,
|
|
154
|
+
model: config.model,
|
|
155
|
+
raw: data,
|
|
156
|
+
};
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e && typeof e === "object" && "code" in e) throw e;
|
|
159
|
+
throw createAgentError("model_unavailable", e instanceof Error ? e.message : String(e), { cause: e, recoverable: false });
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// openai() passes baseUrl WITHOUT /v1; openaiCompatible() appends /v1/chat/completions
|
|
166
|
+
export function openai(config: {
|
|
167
|
+
apiKey: string;
|
|
168
|
+
model: string;
|
|
169
|
+
maxTokens?: number;
|
|
170
|
+
}): AgentModel {
|
|
171
|
+
return openaiCompatible({
|
|
172
|
+
apiKey: config.apiKey,
|
|
173
|
+
baseUrl: "https://api.openai.com",
|
|
174
|
+
model: config.model,
|
|
175
|
+
maxTokens: config.maxTokens,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Provider-neutral types for the LLM adapter interface.
|
|
2
|
+
//
|
|
3
|
+
// These types bridge the Rust agent core and any specific provider implementation.
|
|
4
|
+
|
|
5
|
+
import type { ToolDefinition } from "../../../pi_host_web.js";
|
|
6
|
+
|
|
7
|
+
/** Context from the Rust agent for a stream_llm action. */
|
|
8
|
+
export interface LlmRequest {
|
|
9
|
+
system_prompt: string;
|
|
10
|
+
messages: AgentMessageShape[];
|
|
11
|
+
tools: ToolDefinition[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The shape of a message as it flows through the Rust core. */
|
|
15
|
+
export type AgentMessageShape =
|
|
16
|
+
| { role: "user"; content: ContentBlock[]; timestamp: number }
|
|
17
|
+
| {
|
|
18
|
+
role: "assistant";
|
|
19
|
+
content: ContentBlock[];
|
|
20
|
+
api: string;
|
|
21
|
+
provider: string;
|
|
22
|
+
model: string;
|
|
23
|
+
stop_reason: string;
|
|
24
|
+
error_message: string | null;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
usage: TokenUsage;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
role: "tool_result";
|
|
30
|
+
tool_call_id: string;
|
|
31
|
+
tool_name: string;
|
|
32
|
+
content: ContentBlock[];
|
|
33
|
+
details: unknown;
|
|
34
|
+
is_error: boolean;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface ContentBlock {
|
|
39
|
+
type: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
arguments?: Record<string, unknown>;
|
|
44
|
+
media_type?: string;
|
|
45
|
+
data?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TokenUsage {
|
|
49
|
+
input: number;
|
|
50
|
+
output: number;
|
|
51
|
+
cache_read: number;
|
|
52
|
+
cache_write: number;
|
|
53
|
+
total_tokens: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Result from the provider — fed back into Rust via onLlmDone. */
|
|
57
|
+
export interface ProviderResult {
|
|
58
|
+
/** The LlmResult JSON to pass to onLlmDone. */
|
|
59
|
+
llmResult: object;
|
|
60
|
+
/** Chunks to feed via feedLlmChunk before calling onLlmDone. */
|
|
61
|
+
chunks: object[];
|
|
62
|
+
/** Log entries from the provider call. */
|
|
63
|
+
log: string[];
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// IndexedDB wrapper — converts internal IndexedDBSessionBackend to public AgentStore.
|
|
2
|
+
|
|
3
|
+
import { IndexedDBSessionBackend } from "./persistence.ts";
|
|
4
|
+
import type { AgentStore, AgentSnapshot } from "../../types.ts";
|
|
5
|
+
import type { PersistData } from "../../../pi_host_web.js";
|
|
6
|
+
|
|
7
|
+
export function indexedDbStore(): AgentStore {
|
|
8
|
+
const backend = new IndexedDBSessionBackend();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
async loadSession(sessionId: string): Promise<AgentSnapshot | null> {
|
|
12
|
+
const data = await backend.load(sessionId);
|
|
13
|
+
if (!data) return null;
|
|
14
|
+
return { version: 1, data } as AgentSnapshot;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async saveSession(sessionId: string, snapshot: AgentSnapshot): Promise<void> {
|
|
18
|
+
await backend.save(sessionId, snapshot.data as unknown as PersistData);
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Artifact methods are not supported by the raw IndexedDBSessionBackend.
|
|
22
|
+
// If artifact support is needed, use a store that implements them.
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session persistence backend for browser agents.
|
|
3
|
+
*
|
|
4
|
+
* Uses PersistData for the new Agent lifecycle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PersistData } from "../../../pi_host_web.js";
|
|
8
|
+
|
|
9
|
+
export interface SessionBackend {
|
|
10
|
+
save(sessionId: string, state: PersistData): Promise<void>;
|
|
11
|
+
load(sessionId: string): Promise<PersistData | null>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DB_NAME = "pi-oxide-browser-agent";
|
|
15
|
+
const DB_VERSION = 3;
|
|
16
|
+
const STORE_NAME = "sessions";
|
|
17
|
+
|
|
18
|
+
export class IndexedDBSessionBackend implements SessionBackend {
|
|
19
|
+
private dbPromise: Promise<IDBDatabase>;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.dbPromise = this.openDB();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private openDB(): Promise<IDBDatabase> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
28
|
+
req.onupgradeneeded = () => {
|
|
29
|
+
const db = req.result;
|
|
30
|
+
if (db.objectStoreNames.contains("session")) {
|
|
31
|
+
db.deleteObjectStore("session");
|
|
32
|
+
}
|
|
33
|
+
if (db.objectStoreNames.contains("artifacts")) {
|
|
34
|
+
db.deleteObjectStore("artifacts");
|
|
35
|
+
}
|
|
36
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
37
|
+
db.createObjectStore(STORE_NAME, { keyPath: "sessionId" });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
req.onsuccess = () => resolve(req.result);
|
|
41
|
+
req.onerror = () => reject(req.error);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async save(sessionId: string, state: PersistData): Promise<void> {
|
|
46
|
+
const db = await this.dbPromise;
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
49
|
+
tx.objectStore(STORE_NAME).put({
|
|
50
|
+
sessionId,
|
|
51
|
+
state,
|
|
52
|
+
updatedAt: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
tx.oncomplete = () => resolve();
|
|
55
|
+
tx.onerror = () => reject(tx.error);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async load(sessionId: string): Promise<PersistData | null> {
|
|
60
|
+
const db = await this.dbPromise;
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
63
|
+
const req = tx.objectStore(STORE_NAME).get(sessionId);
|
|
64
|
+
req.onsuccess = () => {
|
|
65
|
+
const result = req.result as { state: PersistData } | undefined;
|
|
66
|
+
resolve(result?.state ?? null);
|
|
67
|
+
};
|
|
68
|
+
req.onerror = () => reject(req.error);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// artifactTools() pack — wraps createArtifactToolRegistry from service.ts.
|
|
2
|
+
// Hides host handles; the actual handle is wired at build time by ToolRegistryBuilder.
|
|
3
|
+
|
|
4
|
+
import type { AgentTools, AgentToolDefinition } from "../../types.ts";
|
|
5
|
+
import { ARTIFACT_TOOLS } from "./service.ts";
|
|
6
|
+
|
|
7
|
+
export function artifactTools(): AgentTools {
|
|
8
|
+
const definitions: AgentToolDefinition[] = ARTIFACT_TOOLS.map((t) => ({
|
|
9
|
+
name: t.name,
|
|
10
|
+
description: t.description,
|
|
11
|
+
inputSchema: t.parameters,
|
|
12
|
+
run: () => {
|
|
13
|
+
throw new Error("artifactTools handlers are wired at build time by ToolRegistryBuilder");
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
definitions,
|
|
19
|
+
getHandler() {
|
|
20
|
+
// Handlers are provided by createArtifactToolRegistry at build time.
|
|
21
|
+
return null;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|