@guuey/host 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 +47 -0
- package/dist/agent-entry.d.ts +38 -0
- package/dist/agent-entry.d.ts.map +1 -0
- package/dist/agent-entry.js +87 -0
- package/dist/boot-context.d.ts +50 -0
- package/dist/boot-context.d.ts.map +1 -0
- package/dist/boot-context.js +22 -0
- package/dist/creds.d.ts +26 -0
- package/dist/creds.d.ts.map +1 -0
- package/dist/creds.js +45 -0
- package/dist/frameworks/claude-options.d.ts +149 -0
- package/dist/frameworks/claude-options.d.ts.map +1 -0
- package/dist/frameworks/claude-options.js +208 -0
- package/dist/frameworks/claude-runner.d.ts +3 -0
- package/dist/frameworks/claude-runner.d.ts.map +1 -0
- package/dist/frameworks/claude-runner.js +33 -0
- package/dist/frameworks/claude.d.ts +72 -0
- package/dist/frameworks/claude.d.ts.map +1 -0
- package/dist/frameworks/claude.js +80 -0
- package/dist/frameworks/google-adk.d.ts +105 -0
- package/dist/frameworks/google-adk.d.ts.map +1 -0
- package/dist/frameworks/google-adk.js +269 -0
- package/dist/frameworks/openai-runner.d.ts +3 -0
- package/dist/frameworks/openai-runner.d.ts.map +1 -0
- package/dist/frameworks/openai-runner.js +37 -0
- package/dist/frameworks/openai.d.ts +65 -0
- package/dist/frameworks/openai.d.ts.map +1 -0
- package/dist/frameworks/openai.js +207 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +134 -0
- package/dist/options.d.ts +173 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +248 -0
- package/dist/preamble.d.ts +16 -0
- package/dist/preamble.d.ts.map +1 -0
- package/dist/preamble.js +34 -0
- package/dist/run-openai.d.ts +65 -0
- package/dist/run-openai.d.ts.map +1 -0
- package/dist/run-openai.js +207 -0
- package/dist/run.d.ts +72 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +80 -0
- package/dist/sdk-version.d.ts +5 -0
- package/dist/sdk-version.d.ts.map +1 -0
- package/dist/sdk-version.js +56 -0
- package/package.json +78 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI (`@openai/agents`) arm of the universal host loop. Mirrors
|
|
3
|
+
* `run.ts`'s `runInvoke` shape: one invoke → an `@openai/agents` `Agent` built
|
|
4
|
+
* from the snapshot, run streamed, each raw `RunStreamEvent` emitted to fd-3 as
|
|
5
|
+
* a `native('openai-agents-sdk', …)` WorkerEvent (the Router dispatches them to
|
|
6
|
+
* the `@silverprotocol/openai-agents` normalizer). On clean completion it emits
|
|
7
|
+
* `done`; on `MaxTurnsExceededError` it emits the `__host_error__` sentinel the
|
|
8
|
+
* normalizer maps to `turn.error` (code `max_turns`), THEN `done(..,"max_turns")`;
|
|
9
|
+
* on any other failure it emits `error`. Never throws — every failure path is a
|
|
10
|
+
* terminal event so the Router always sees one. Emits `hello` FIRST (§8 item B)
|
|
11
|
+
* — the SDK-version handshake.
|
|
12
|
+
*
|
|
13
|
+
* `run` (the `@openai/agents` runner) is injected so the loop is unit-testable
|
|
14
|
+
* without a live model — the entrypoint (`index.ts`) passes the real SDK `run`.
|
|
15
|
+
*
|
|
16
|
+
* Snapshot → Agent mapping:
|
|
17
|
+
* - `model` → `snapshot.model` (or the SDK's own default when absent —
|
|
18
|
+
* NOT Claude's; this is the OpenAI path).
|
|
19
|
+
* - `instructions` → `systemPrompt` + the §1.4 context preamble (reuses
|
|
20
|
+
* `withContextPreamble`, identical to the Claude path).
|
|
21
|
+
* - `mcpServers` → the framework-neutral `resolveMcpServers` output (same
|
|
22
|
+
* federation/env-substitution as Claude), each `http`/`sse`
|
|
23
|
+
* entry → an `MCPServerStreamableHttp` carrying its
|
|
24
|
+
* `Authorization` (and any other) header via `requestInit`.
|
|
25
|
+
* - `maxTurns` → `snapshot.runtime?.maxTurns` (passed to `run(...)`; the SDK
|
|
26
|
+
* THROWS `MaxTurnsExceededError` from `stream.completed`).
|
|
27
|
+
* - API key → `OPENAI_API_KEY` (the Router sets it at pod boot), applied
|
|
28
|
+
* globally via `setDefaultOpenAIKey` by the entrypoint.
|
|
29
|
+
*/
|
|
30
|
+
import { Agent, type RunStreamEvent } from "@openai/agents";
|
|
31
|
+
import type { Emitter } from "@guuey/worker";
|
|
32
|
+
import type { HostInvoke, HostRuntime } from "./run.js";
|
|
33
|
+
import { type GuueyAgent } from "@guuey/config";
|
|
34
|
+
/**
|
|
35
|
+
* The streamed-result surface the loop CONSUMES — exactly the three members it
|
|
36
|
+
* reads off the `@openai/agents` `StreamedRunResult`: async iteration over raw
|
|
37
|
+
* events, `completed` (which THROWS `MaxTurnsExceededError`), and the resolved
|
|
38
|
+
* `finalOutput`. A minimal structural interface (NOT the full `StreamedRunResult`
|
|
39
|
+
* class) so the real runner result AND a test fake both satisfy it without a cast
|
|
40
|
+
* — `StreamedRunResult` has private fields, so a plain object could never match
|
|
41
|
+
* the class, but it DOES match this read-only projection.
|
|
42
|
+
*/
|
|
43
|
+
export interface OpenaiRunResult extends AsyncIterable<RunStreamEvent> {
|
|
44
|
+
readonly completed: Promise<void>;
|
|
45
|
+
/** Resolved text output for the default text-output Agent (or `undefined`). */
|
|
46
|
+
readonly finalOutput?: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `run` surface the loop needs — the real `@openai/agents` `run` (streamed
|
|
50
|
+
* overload) satisfies it (its `StreamedRunResult` is assignable to
|
|
51
|
+
* {@link OpenaiRunResult}). Injected so the loop is testable without a live model.
|
|
52
|
+
*/
|
|
53
|
+
export type OpenaiRunFn = (agent: Agent, input: string, options: {
|
|
54
|
+
stream: true;
|
|
55
|
+
maxTurns?: number;
|
|
56
|
+
}) => Promise<OpenaiRunResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Run one invoke through `@openai/agents`. Emits `native` per `RunStreamEvent`,
|
|
59
|
+
* `done` on completion (with the `max_turns` sentinel + reason on a turn-cap),
|
|
60
|
+
* `error` on a build failure or any non-max-turns throw. Never throws.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runInvokeOpenai(snapshot: GuueyAgent & {
|
|
63
|
+
framework?: string;
|
|
64
|
+
}, invoke: HostInvoke, runtime: HostRuntime, emit: Emitter, run: OpenaiRunFn): Promise<void>;
|
|
65
|
+
//# sourceMappingURL=run-openai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-openai.d.ts","sourceRoot":"","sources":["../src/run-openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EACL,KAAK,EAIL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,eAAe,CAAC;AAOxD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAQ7E;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAgB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACpE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KACzC,OAAO,CAAC,eAAe,CAAC,CAAC;AAgB9B;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7C,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,IAAI,CAAC,CA4Gf"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI (`@openai/agents`) arm of the universal host loop. Mirrors
|
|
3
|
+
* `run.ts`'s `runInvoke` shape: one invoke → an `@openai/agents` `Agent` built
|
|
4
|
+
* from the snapshot, run streamed, each raw `RunStreamEvent` emitted to fd-3 as
|
|
5
|
+
* a `native('openai-agents-sdk', …)` WorkerEvent (the Router dispatches them to
|
|
6
|
+
* the `@silverprotocol/openai-agents` normalizer). On clean completion it emits
|
|
7
|
+
* `done`; on `MaxTurnsExceededError` it emits the `__host_error__` sentinel the
|
|
8
|
+
* normalizer maps to `turn.error` (code `max_turns`), THEN `done(..,"max_turns")`;
|
|
9
|
+
* on any other failure it emits `error`. Never throws — every failure path is a
|
|
10
|
+
* terminal event so the Router always sees one. Emits `hello` FIRST (§8 item B)
|
|
11
|
+
* — the SDK-version handshake.
|
|
12
|
+
*
|
|
13
|
+
* `run` (the `@openai/agents` runner) is injected so the loop is unit-testable
|
|
14
|
+
* without a live model — the entrypoint (`index.ts`) passes the real SDK `run`.
|
|
15
|
+
*
|
|
16
|
+
* Snapshot → Agent mapping:
|
|
17
|
+
* - `model` → `snapshot.model` (or the SDK's own default when absent —
|
|
18
|
+
* NOT Claude's; this is the OpenAI path).
|
|
19
|
+
* - `instructions` → `systemPrompt` + the §1.4 context preamble (reuses
|
|
20
|
+
* `withContextPreamble`, identical to the Claude path).
|
|
21
|
+
* - `mcpServers` → the framework-neutral `resolveMcpServers` output (same
|
|
22
|
+
* federation/env-substitution as Claude), each `http`/`sse`
|
|
23
|
+
* entry → an `MCPServerStreamableHttp` carrying its
|
|
24
|
+
* `Authorization` (and any other) header via `requestInit`.
|
|
25
|
+
* - `maxTurns` → `snapshot.runtime?.maxTurns` (passed to `run(...)`; the SDK
|
|
26
|
+
* THROWS `MaxTurnsExceededError` from `stream.completed`).
|
|
27
|
+
* - API key → `OPENAI_API_KEY` (the Router sets it at pod boot), applied
|
|
28
|
+
* globally via `setDefaultOpenAIKey` by the entrypoint.
|
|
29
|
+
*/
|
|
30
|
+
import { Agent, MaxTurnsExceededError, MCPServerStreamableHttp, } from "@openai/agents";
|
|
31
|
+
import { resolveMcpServers, withContextPreamble, } from "./options.js";
|
|
32
|
+
import { GUUEY_DEFAULT_SYSTEM_PROMPT } from "@guuey/config";
|
|
33
|
+
import { resolveSdkVersion } from "./sdk-version.js";
|
|
34
|
+
/** The framework tag this arm runs — matches the `AgentFramework` enum value. */
|
|
35
|
+
const OPENAI_FRAMEWORK = "openai-agents-sdk";
|
|
36
|
+
/** The npm package whose installed version is this framework's `hello.sdkVersion`. */
|
|
37
|
+
const OPENAI_SDK_PACKAGE = "@openai/agents";
|
|
38
|
+
/**
|
|
39
|
+
* Run one invoke through `@openai/agents`. Emits `native` per `RunStreamEvent`,
|
|
40
|
+
* `done` on completion (with the `max_turns` sentinel + reason on a turn-cap),
|
|
41
|
+
* `error` on a build failure or any non-max-turns throw. Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export async function runInvokeOpenai(snapshot, invoke, runtime, emit, run) {
|
|
44
|
+
// The SDK-version handshake (§8 item B, additive-optional) — ALWAYS the
|
|
45
|
+
// first event of this invoke's fd-3 stream, before any native/turn event.
|
|
46
|
+
// `sdkVersion` is resolved at runtime, never hardcoded; `null` (SDK not
|
|
47
|
+
// resolvable) is tolerated by the Router.
|
|
48
|
+
emit.hello(OPENAI_FRAMEWORK, OPENAI_SDK_PACKAGE, resolveSdkVersion(OPENAI_SDK_PACKAGE));
|
|
49
|
+
// The host runs inside the Router's bubblewrap jail with NO IRSA; a federated
|
|
50
|
+
// MCP server reads its Router-written credential file. `resolveMcpServers`
|
|
51
|
+
// needs the same per-invoke context the Claude path builds.
|
|
52
|
+
const ctx = {
|
|
53
|
+
input: invoke.input,
|
|
54
|
+
identity: invoke.identity,
|
|
55
|
+
fs: invoke.fs,
|
|
56
|
+
history: invoke.history,
|
|
57
|
+
listCredentials: runtime.listCredentials,
|
|
58
|
+
...(runtime.apiKey !== undefined ? { apiKey: runtime.apiKey } : {}),
|
|
59
|
+
...(invoke.priorMemory !== undefined ? { priorMemory: invoke.priorMemory } : {}),
|
|
60
|
+
...(invoke.priorState !== undefined ? { priorState: invoke.priorState } : {}),
|
|
61
|
+
};
|
|
62
|
+
// Build the Agent (+ connect its MCP servers). A build failure (e.g. an
|
|
63
|
+
// unresolved {file} system prompt, or an unsupported MCP arm) is a terminal
|
|
64
|
+
// `error` — never a throw out of this function.
|
|
65
|
+
let instructions;
|
|
66
|
+
let mcpServers;
|
|
67
|
+
try {
|
|
68
|
+
// Snapshots reach the worker fully resolved — the CLI inlines any `{file}`
|
|
69
|
+
// system prompt before upload. A non-string reaching the worker means a
|
|
70
|
+
// direct API hit with an un-resolved snapshot; reject loudly (same as Claude).
|
|
71
|
+
if (snapshot.systemPrompt !== undefined && typeof snapshot.systemPrompt !== "string") {
|
|
72
|
+
throw new Error(`@guuey/host: snapshot.systemPrompt must be a resolved string (got ${JSON.stringify(snapshot.systemPrompt)}). The CLI inlines {file} references before upload; workers never read the filesystem.`);
|
|
73
|
+
}
|
|
74
|
+
instructions = withContextPreamble(snapshot.systemPrompt ?? GUUEY_DEFAULT_SYSTEM_PROMPT, ctx.history, ctx.priorMemory, ctx.priorState);
|
|
75
|
+
mcpServers = buildOpenaiMcpServers(ctx);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
emit.error(err instanceof Error ? err.message : String(err));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
for (const server of mcpServers) {
|
|
83
|
+
await server.connect();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
await closeAll(mcpServers);
|
|
88
|
+
emit.error(err instanceof Error ? err.message : String(err));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const maxTurns = snapshot.runtime?.maxTurns;
|
|
93
|
+
// `MCPServerStreamableHttp` IS an `MCPServer`; the Agent's `mcpServers` field
|
|
94
|
+
// is `MCPServer[]` — widen via the interface (no cast; structural subtype).
|
|
95
|
+
const servers = mcpServers;
|
|
96
|
+
const agent = new Agent({
|
|
97
|
+
name: "guuey-agent",
|
|
98
|
+
instructions,
|
|
99
|
+
mcpServers: servers,
|
|
100
|
+
...(snapshot.model !== undefined ? { model: snapshot.model } : {}),
|
|
101
|
+
});
|
|
102
|
+
const stream = await run(agent, invoke.input, {
|
|
103
|
+
stream: true,
|
|
104
|
+
...(maxTurns !== undefined ? { maxTurns } : {}),
|
|
105
|
+
});
|
|
106
|
+
// Drive the stream: each raw event crosses the wire as native.
|
|
107
|
+
for await (const ev of stream) {
|
|
108
|
+
emit.native(OPENAI_FRAMEWORK, toJson(ev));
|
|
109
|
+
}
|
|
110
|
+
// The runner THROWS `MaxTurnsExceededError` from `completed` AFTER the stream
|
|
111
|
+
// ends. Catch it specifically → feed the normalizer the `__host_error__`
|
|
112
|
+
// sentinel, then a terminal `done(.., "max_turns")`. Other throws → `error`.
|
|
113
|
+
try {
|
|
114
|
+
await stream.completed;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof MaxTurnsExceededError) {
|
|
118
|
+
// The `__host_error__` sentinel — a fresh object literal so it is checked
|
|
119
|
+
// directly against `JsonValue` (the `native` payload type). Shape is the
|
|
120
|
+
// BINDING contract {@link HostErrorSentinel}.
|
|
121
|
+
emit.native(OPENAI_FRAMEWORK, {
|
|
122
|
+
type: "__host_error__",
|
|
123
|
+
code: "max_turns",
|
|
124
|
+
message: err.message,
|
|
125
|
+
});
|
|
126
|
+
emit.done(finalText(stream), "max_turns");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
emit.done(finalText(stream), "end_turn");
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
emit.error(err instanceof Error ? err.message : String(err));
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await closeAll(mcpServers);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The agent's final text output, or `""` when absent (a tool-only turn, or a
|
|
142
|
+
* turn cut short by `max_turns`). For the default text-output Agent `finalOutput`
|
|
143
|
+
* is `string | undefined`.
|
|
144
|
+
*/
|
|
145
|
+
function finalText(stream) {
|
|
146
|
+
const out = stream.finalOutput;
|
|
147
|
+
return typeof out === "string" ? out : "";
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Translate the framework-neutral resolved MCP map → connected-capable
|
|
151
|
+
* `MCPServerStreamableHttp` instances. Each `http`/`sse` entry's `headers`
|
|
152
|
+
* (typically `{ authorization: 'Bearer <token>' }`, from the federation
|
|
153
|
+
* credential file or `${env.NAME}` substitution) ride on the underlying MCP
|
|
154
|
+
* transport's `RequestInit.headers` via `requestInit`.
|
|
155
|
+
*
|
|
156
|
+
* The cred dir only yields `http`/`sse` servers (the Router resolves all
|
|
157
|
+
* transport to one of those two); the `stdio` arm below is unreachable
|
|
158
|
+
* defensive code — kept so a future schema change can't silently drop a server.
|
|
159
|
+
*/
|
|
160
|
+
function buildOpenaiMcpServers(ctx) {
|
|
161
|
+
const resolved = resolveMcpServers(ctx);
|
|
162
|
+
const servers = [];
|
|
163
|
+
for (const [name, entry] of Object.entries(resolved)) {
|
|
164
|
+
servers.push(toOpenaiMcpServer(name, entry));
|
|
165
|
+
}
|
|
166
|
+
return servers;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One resolved `SdkMcpServer` → an `MCPServerStreamableHttp`. Both `http` and
|
|
170
|
+
* `sse` arms map to the StreamableHTTP server (the SDK's HTTP MCP transport);
|
|
171
|
+
* the `stdio` arm is unreachable (`resolveMcpServers` rejects colocated before
|
|
172
|
+
* we get here) — handled with a loud throw so a future schema change can't
|
|
173
|
+
* silently drop a server.
|
|
174
|
+
*/
|
|
175
|
+
function toOpenaiMcpServer(name, entry) {
|
|
176
|
+
if (entry.type === "stdio") {
|
|
177
|
+
throw new Error(`mcpServers["${name}"]: stdio (colocated) MCP is not supported on the OpenAI host path.`);
|
|
178
|
+
}
|
|
179
|
+
const headers = entry.headers;
|
|
180
|
+
return new MCPServerStreamableHttp({
|
|
181
|
+
url: entry.url,
|
|
182
|
+
name,
|
|
183
|
+
...(headers && Object.keys(headers).length > 0 ? { requestInit: { headers } } : {}),
|
|
184
|
+
// `customDataExtractor` (agents 0.12+) is the ONLY channel that carries an
|
|
185
|
+
// MCP tool result's `structuredContent` onto the wire (`item.customData`)
|
|
186
|
+
// WITHOUT leaking it into model-visible text — without this, the ggui cache
|
|
187
|
+
// marker never reaches the normalizer and render metering goes blind.
|
|
188
|
+
// Mirrors the verified silverprotocol capture-agent wiring; the facet reads
|
|
189
|
+
// `item.customData.structuredContent`.
|
|
190
|
+
customDataExtractor: (context) => context.structuredContent !== undefined
|
|
191
|
+
? { structuredContent: context.structuredContent }
|
|
192
|
+
: undefined,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** Best-effort close of every connected MCP server (release the transport). */
|
|
196
|
+
async function closeAll(servers) {
|
|
197
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Coerce one `RunStreamEvent` to the `JsonValue` the `native` event carries.
|
|
201
|
+
* Stream events are plain JSON-serializable objects; the round-trip drops any
|
|
202
|
+
* non-JSON surface and yields the exact shape the Router's normalizer parses off
|
|
203
|
+
* the wire. Mirrors `run.ts`'s `toJson`.
|
|
204
|
+
*/
|
|
205
|
+
function toJson(ev) {
|
|
206
|
+
return JSON.parse(JSON.stringify(ev));
|
|
207
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The testable core of the worker loop. `runInvoke` runs one invoke through the
|
|
3
|
+
* Claude Agent SDK and emits each native `SDKMessage` to fd-3 as a `native`
|
|
4
|
+
* WorkerEvent (the Router dispatches them to the matching normalizer). On the
|
|
5
|
+
* SDK result message it emits `done`; on a throw or a framework-gate violation
|
|
6
|
+
* it emits `error`. Emits `hello` FIRST (§8 item B) — the SDK-version handshake.
|
|
7
|
+
*
|
|
8
|
+
* `query` is injected so the loop is unit-testable without a live model. The
|
|
9
|
+
* worker entrypoint (`index.ts`) passes the real `@anthropic-ai/claude-agent-sdk`
|
|
10
|
+
* `query`.
|
|
11
|
+
*/
|
|
12
|
+
import type { Options, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
13
|
+
import type { Emitter, Fs, HistoryMessage, Identity, JsonValue } from "@guuey/worker";
|
|
14
|
+
import { buildOptions, type CredentialFile, type PriorMemoryRecord } from "./options.js";
|
|
15
|
+
/**
|
|
16
|
+
* The invoke this host consumes. A superset of `@guuey/worker`'s `Invoke`:
|
|
17
|
+
* `priorMemory`/`priorState` are the §1.4 push-by-value context the worker reads
|
|
18
|
+
* for the preamble (the Worker Protocol `Invoke` is EXTENDED with these in
|
|
19
|
+
* Task 3; until then the worker loop parses them off the raw control line).
|
|
20
|
+
*/
|
|
21
|
+
export interface HostInvoke {
|
|
22
|
+
input: string;
|
|
23
|
+
identity: Identity;
|
|
24
|
+
fs: Fs;
|
|
25
|
+
history: HistoryMessage[];
|
|
26
|
+
priorMemory?: PriorMemoryRecord[];
|
|
27
|
+
priorState?: JsonValue;
|
|
28
|
+
}
|
|
29
|
+
/** Per-process config the worker resolves once at boot. */
|
|
30
|
+
export interface HostRuntime {
|
|
31
|
+
/**
|
|
32
|
+
* Anthropic API key — local-dev fallback when `baseUrl`+`authToken` are
|
|
33
|
+
* absent. One of (`baseUrl`+`authToken`) or `apiKey` must be provided;
|
|
34
|
+
* `buildOptions` throws at invoke time if neither is present.
|
|
35
|
+
*/
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Loopback proxy base URL (hosted/broker mode). Task 8 injects this as
|
|
39
|
+
* `ANTHROPIC_BASE_URL` via `buildWorkerEnv`. When set together with
|
|
40
|
+
* `authToken`, the Claude CLI subprocess routes through the managed-LLM
|
|
41
|
+
* broker; the real API key is intentionally absent to prevent leaks.
|
|
42
|
+
*/
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Opaque session token for the loopback proxy (hosted/broker mode). Task 8
|
|
46
|
+
* injects this as `ANTHROPIC_AUTH_TOKEN`. Required when `baseUrl` is set.
|
|
47
|
+
*/
|
|
48
|
+
authToken?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Returns every credential the Router broker wrote to
|
|
51
|
+
* `<sessionDir>/.guuey/credentials/` this invoke. Injected so the run path
|
|
52
|
+
* stays pure (no disk access inside `runInvoke`).
|
|
53
|
+
*/
|
|
54
|
+
listCredentials: () => Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
cred: CredentialFile;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
/** The `query` surface the loop needs — the real SDK `query` satisfies it. */
|
|
60
|
+
export type QueryFn = (params: {
|
|
61
|
+
prompt: string;
|
|
62
|
+
options: Options;
|
|
63
|
+
}) => AsyncIterable<SDKMessage>;
|
|
64
|
+
/**
|
|
65
|
+
* Run one invoke. Emits `native` per SDKMessage, `done` on the result, `error`
|
|
66
|
+
* on a framework-gate violation or a thrown error. Never throws — every failure
|
|
67
|
+
* path becomes an `error` event so the Router always sees a terminal event.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runInvoke(snapshot: {
|
|
70
|
+
framework?: string;
|
|
71
|
+
} & Parameters<typeof buildOptions>[0], invoke: HostInvoke, runtime: HostRuntime, emit: Emitter, query: QueryFn): Promise<void>;
|
|
72
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAc,MAAM,eAAe,CAAC;AAClG,OAAO,EACL,YAAY,EAEZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAQtB;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,EAAE,MAAM,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,8EAA8E;AAC9E,MAAM,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC;AA0BhC;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,EACrE,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { buildOptions, } from "./options.js";
|
|
2
|
+
import { resolveSdkVersion } from "./sdk-version.js";
|
|
3
|
+
/** The framework THIS run path runs. OpenAI has its own path (`run-openai.ts`). */
|
|
4
|
+
const CLAUDE_FRAMEWORK = "claude-agent-sdk";
|
|
5
|
+
/** The npm package whose installed version is this framework's `hello.sdkVersion`. */
|
|
6
|
+
const CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
function isResultMessage(msg) {
|
|
8
|
+
return msg.type === "result";
|
|
9
|
+
}
|
|
10
|
+
/** Map the SDK result message → the Worker Protocol `done` stopReason + result. */
|
|
11
|
+
function resultToDone(msg) {
|
|
12
|
+
const result = typeof msg.result === "string" ? msg.result : "";
|
|
13
|
+
const stopReason = msg.subtype === "success"
|
|
14
|
+
? "end_turn"
|
|
15
|
+
: msg.subtype === "error_max_turns"
|
|
16
|
+
? "max_turns"
|
|
17
|
+
: "error";
|
|
18
|
+
return { stopReason, result };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run one invoke. Emits `native` per SDKMessage, `done` on the result, `error`
|
|
22
|
+
* on a framework-gate violation or a thrown error. Never throws — every failure
|
|
23
|
+
* path becomes an `error` event so the Router always sees a terminal event.
|
|
24
|
+
*/
|
|
25
|
+
export async function runInvoke(snapshot, invoke, runtime, emit, query) {
|
|
26
|
+
// The SDK-version handshake (§8 item B, additive-optional) — ALWAYS the
|
|
27
|
+
// first event of this invoke's fd-3 stream, before the framework gate or any
|
|
28
|
+
// native/turn event. `sdkVersion` is resolved at runtime, never hardcoded;
|
|
29
|
+
// `null` (SDK not resolvable) is tolerated by the Router.
|
|
30
|
+
emit.hello(CLAUDE_FRAMEWORK, CLAUDE_SDK_PACKAGE, resolveSdkVersion(CLAUDE_SDK_PACKAGE));
|
|
31
|
+
// Framework gate: this run path is the Claude SDK. OpenAI agents route to
|
|
32
|
+
// `runInvokeOpenai` (selected in `index.ts` by `snapshot.framework`) and never
|
|
33
|
+
// reach here. A non-claude framework arriving here (e.g. `google-adk`,
|
|
34
|
+
// `vanilla`) has no run path yet → a clear `error`, never a silent mis-run.
|
|
35
|
+
if (snapshot.framework && snapshot.framework !== CLAUDE_FRAMEWORK) {
|
|
36
|
+
emit.error(`@guuey/host: the claude run path got framework '${snapshot.framework}'; no run path for it yet.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let options;
|
|
40
|
+
try {
|
|
41
|
+
const ctx = {
|
|
42
|
+
input: invoke.input,
|
|
43
|
+
identity: invoke.identity,
|
|
44
|
+
fs: invoke.fs,
|
|
45
|
+
history: invoke.history,
|
|
46
|
+
listCredentials: runtime.listCredentials,
|
|
47
|
+
...(runtime.apiKey !== undefined ? { apiKey: runtime.apiKey } : {}),
|
|
48
|
+
...(runtime.baseUrl !== undefined ? { baseUrl: runtime.baseUrl } : {}),
|
|
49
|
+
...(runtime.authToken !== undefined ? { authToken: runtime.authToken } : {}),
|
|
50
|
+
...(invoke.priorMemory !== undefined ? { priorMemory: invoke.priorMemory } : {}),
|
|
51
|
+
...(invoke.priorState !== undefined ? { priorState: invoke.priorState } : {}),
|
|
52
|
+
};
|
|
53
|
+
options = buildOptions(snapshot, ctx);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
emit.error(err instanceof Error ? err.message : String(err));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
let done;
|
|
61
|
+
for await (const msg of query({ prompt: invoke.input, options })) {
|
|
62
|
+
emit.native(CLAUDE_FRAMEWORK, toJson(msg));
|
|
63
|
+
if (isResultMessage(msg))
|
|
64
|
+
done = resultToDone(msg);
|
|
65
|
+
}
|
|
66
|
+
emit.done(done?.result ?? "", done?.stopReason ?? "end_turn");
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
emit.error(err instanceof Error ? err.message : String(err));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Coerce one SDKMessage to the `JsonValue` the `native` event carries. SDK
|
|
74
|
+
* messages are plain JSON-serializable objects; the round-trip drops any
|
|
75
|
+
* non-JSON surface (functions/symbols never appear on them) and yields the
|
|
76
|
+
* exact shape the Router's normalizer parses off the wire.
|
|
77
|
+
*/
|
|
78
|
+
function toJson(msg) {
|
|
79
|
+
return JSON.parse(JSON.stringify(msg));
|
|
80
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Reads the installed `pkgName` package's own `package.json#version`, or
|
|
2
|
+
* `null` when the package isn't installed / its version can't be resolved
|
|
3
|
+
* (never throws — a missing SDK is tolerated, not fatal). */
|
|
4
|
+
export declare function resolveSdkVersion(pkgName: string, anchorPath?: string): string | null;
|
|
5
|
+
//# sourceMappingURL=sdk-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-version.d.ts","sourceRoot":"","sources":["../src/sdk-version.ts"],"names":[],"mappings":"AA0BA;;8DAE8D;AAC9D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BrF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves an installed SDK package's own `package.json#version` at RUNTIME —
|
|
3
|
+
* the real resolved version (never a hardcoded literal, and never the
|
|
4
|
+
* `package.json` dependency RANGE, which may be a caret). Feeds the worker
|
|
5
|
+
* hello handshake's `sdkVersion` field (§8 item B).
|
|
6
|
+
*
|
|
7
|
+
* Mirrors silverprotocol's `resolveSdkVersion`
|
|
8
|
+
* (`sdks/typescript/packages/e2e/src/capture-cli.ts`): `require.resolve(pkg)`
|
|
9
|
+
* finds the package's main entry — `require.resolve(`${pkg}/package.json`)`
|
|
10
|
+
* (the naive approach) THROWS `ERR_PACKAGE_PATH_NOT_EXPORTED` for a package
|
|
11
|
+
* whose `exports` map omits a `./package.json` subpath, which is the case for
|
|
12
|
+
* BOTH `@anthropic-ai/claude-agent-sdk` and `@openai/agents` (verified
|
|
13
|
+
* empirically in that playbook run) — so we walk up from the main entry's
|
|
14
|
+
* directory instead, bounded to a few levels (real packages are 0-2 levels
|
|
15
|
+
* deep), until we find the `package.json` whose own `name` matches.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
/** Real packages are 0-2 levels deep from their resolved main entry. */
|
|
23
|
+
const MAX_WALK_UP_DEPTH = 5;
|
|
24
|
+
/** Reads the installed `pkgName` package's own `package.json#version`, or
|
|
25
|
+
* `null` when the package isn't installed / its version can't be resolved
|
|
26
|
+
* (never throws — a missing SDK is tolerated, not fatal). */
|
|
27
|
+
export function resolveSdkVersion(pkgName, anchorPath) {
|
|
28
|
+
try {
|
|
29
|
+
// `anchorPath` re-anchors resolution to another tree (the graceful agent
|
|
30
|
+
// entry's node_modules) — the single-copy rule needs the version of the
|
|
31
|
+
// copy actually driven, not the host's.
|
|
32
|
+
const resolver = anchorPath !== undefined ? createRequire(pathToFileURL(anchorPath).href) : require;
|
|
33
|
+
const mainEntryPath = resolver.resolve(pkgName);
|
|
34
|
+
let dir = dirname(mainEntryPath);
|
|
35
|
+
for (let depth = 0; depth < MAX_WALK_UP_DEPTH; depth++) {
|
|
36
|
+
const candidate = join(dir, "package.json");
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
39
|
+
if (parsed.name === pkgName) {
|
|
40
|
+
return parsed.version ?? null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Not found at this level (or unparsable) — keep walking up.
|
|
45
|
+
}
|
|
46
|
+
const parent = dirname(dir);
|
|
47
|
+
if (parent === dir)
|
|
48
|
+
break; // reached filesystem root
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/host",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The universal config-driven Guuey worker. Reads the resolved agent.json snapshot, runs the Claude Agent SDK, and emits each native SDKMessage to fd-3 as a `native` WorkerEvent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"guuey-host": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@guuey/config": "0.1.1",
|
|
26
|
+
"@guuey/worker": "0.1.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.0.0",
|
|
30
|
+
"typescript": "^5.0.0",
|
|
31
|
+
"vitest": "^3.0.0",
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.199",
|
|
33
|
+
"@openai/agents": "^0.12.0",
|
|
34
|
+
"@google/adk": "^1.3.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"guuey",
|
|
41
|
+
"agent",
|
|
42
|
+
"worker",
|
|
43
|
+
"host",
|
|
44
|
+
"claude-agent-sdk"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
49
|
+
"directory": "packages/host"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://guuey.com",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/loqu-co/guuey/issues"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.199",
|
|
57
|
+
"@google/adk": ">=1.0.0 <2",
|
|
58
|
+
"@openai/agents": "^0.12.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@anthropic-ai/claude-agent-sdk": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@google/adk": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@openai/agents": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "tsc -p tsconfig.build.json",
|
|
73
|
+
"dev": "tsc --watch",
|
|
74
|
+
"typecheck": "tsc --noEmit",
|
|
75
|
+
"test": "vitest run",
|
|
76
|
+
"test:watch": "vitest"
|
|
77
|
+
}
|
|
78
|
+
}
|