@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/agent-entry.d.ts +38 -0
  4. package/dist/agent-entry.d.ts.map +1 -0
  5. package/dist/agent-entry.js +87 -0
  6. package/dist/boot-context.d.ts +50 -0
  7. package/dist/boot-context.d.ts.map +1 -0
  8. package/dist/boot-context.js +22 -0
  9. package/dist/creds.d.ts +26 -0
  10. package/dist/creds.d.ts.map +1 -0
  11. package/dist/creds.js +45 -0
  12. package/dist/frameworks/claude-options.d.ts +149 -0
  13. package/dist/frameworks/claude-options.d.ts.map +1 -0
  14. package/dist/frameworks/claude-options.js +208 -0
  15. package/dist/frameworks/claude-runner.d.ts +3 -0
  16. package/dist/frameworks/claude-runner.d.ts.map +1 -0
  17. package/dist/frameworks/claude-runner.js +33 -0
  18. package/dist/frameworks/claude.d.ts +72 -0
  19. package/dist/frameworks/claude.d.ts.map +1 -0
  20. package/dist/frameworks/claude.js +80 -0
  21. package/dist/frameworks/google-adk.d.ts +105 -0
  22. package/dist/frameworks/google-adk.d.ts.map +1 -0
  23. package/dist/frameworks/google-adk.js +269 -0
  24. package/dist/frameworks/openai-runner.d.ts +3 -0
  25. package/dist/frameworks/openai-runner.d.ts.map +1 -0
  26. package/dist/frameworks/openai-runner.js +37 -0
  27. package/dist/frameworks/openai.d.ts +65 -0
  28. package/dist/frameworks/openai.d.ts.map +1 -0
  29. package/dist/frameworks/openai.js +207 -0
  30. package/dist/index.d.ts +31 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +134 -0
  33. package/dist/options.d.ts +173 -0
  34. package/dist/options.d.ts.map +1 -0
  35. package/dist/options.js +248 -0
  36. package/dist/preamble.d.ts +16 -0
  37. package/dist/preamble.d.ts.map +1 -0
  38. package/dist/preamble.js +34 -0
  39. package/dist/run-openai.d.ts +65 -0
  40. package/dist/run-openai.d.ts.map +1 -0
  41. package/dist/run-openai.js +207 -0
  42. package/dist/run.d.ts +72 -0
  43. package/dist/run.d.ts.map +1 -0
  44. package/dist/run.js +80 -0
  45. package/dist/sdk-version.d.ts +5 -0
  46. package/dist/sdk-version.d.ts.map +1 -0
  47. package/dist/sdk-version.js +56 -0
  48. package/package.json +78 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Google-ADK runner — drives the OFFICIAL `@google/adk` (JS) per invoke and
3
+ * emits every native ADK `Event` to fd-3 for the Router's
4
+ * `createAdkNormalizer`. Replaces the Python ADK host (`guuey_adk_host`)
5
+ * behavior-for-behavior: same hello handshake, same context preamble, same
6
+ * per-invoke `InMemoryRunner` semantics, same final-text extraction.
7
+ *
8
+ * Deliberate mechanics (each one adversarially reviewed):
9
+ *
10
+ * - **Lazy SDK load + single-copy rule.** `@google/adk` is an optional peer,
11
+ * imported at runner creation — never at module top. In graceful mode
12
+ * (`GUUEY_AGENT_ENTRY`, T3) the SDK is resolved from the AGENT ENTRY's own
13
+ * tree via `createRequire(entryUrl)` so the dev's agent and this runner
14
+ * share ONE copy (the dev's version wins in their lane); no-code resolves
15
+ * from the host's own tree (the platform pin in
16
+ * `@guuey-private/host-shared`).
17
+ * - **`role: "user"` pinned on `newMessage`.** Omitting it 400s on tool
18
+ * follow-ups (upstream adk-js#475); the pin is a mechanistically complete
19
+ * mitigation for arbitrary-depth tool loops.
20
+ * - **MCP = Streamable-HTTP only.** The JS `MCPToolset` speaks stdio +
21
+ * Streamable-HTTP; a `transport: "sse"` credential is REJECTED with an
22
+ * actionable error (the Python-era SSE arm has no JS mapping). Auth
23
+ * headers ride `transportOptions.requestInit.headers` (the non-deprecated
24
+ * channel).
25
+ * - **Gemini arming is the documented env pair.** `@google/genai` reads
26
+ * `GOOGLE_GEMINI_BASE_URL` (verified: getBaseUrl in genai 1.52) and the
27
+ * ADK's GoogleLlm reads `GOOGLE_GENAI_API_KEY || GEMINI_API_KEY` — the
28
+ * Router's `buildWorkerEnv` gemini arm injects exactly `GEMINI_API_KEY` +
29
+ * `GOOGLE_GEMINI_BASE_URL`. ADK 1.3.0 exposes no programmatic
30
+ * httpOptions path, so env IS the sanctioned channel here.
31
+ *
32
+ * NEVER rejects to the loop — every failure becomes a terminal `error` event
33
+ * (the wire contract the Python host also kept).
34
+ */
35
+ import { readFileSync } from "node:fs";
36
+ import { createRequire } from "node:module";
37
+ import { dirname, join } from "node:path";
38
+ import { pathToFileURL } from "node:url";
39
+ import { AGENT_ENTRY_ENV, WORKER_ROOT_ENV, loadAgentEntry, materializeAgent, nativeLoad, resolveAgentEntry, } from "../agent-entry.js";
40
+ import { listCredentials } from "../creds.js";
41
+ import { withContextPreamble } from "../preamble.js";
42
+ import { resolveSdkVersion } from "../sdk-version.js";
43
+ const ADK_FRAMEWORK = "google-adk";
44
+ /**
45
+ * Given a require-resolved entry file inside a package, walk up to the
46
+ * package root and return the absolute path of the root export's `import`
47
+ * condition (string conditions only — `@google/adk` 1.3.0's shape), or
48
+ * `undefined` when there is no exports map / no import condition.
49
+ */
50
+ export function importConditionEntry(resolvedEntry) {
51
+ let dir = dirname(resolvedEntry);
52
+ for (let depth = 0; depth < 6; depth++) {
53
+ const pkgPath = join(dir, "package.json");
54
+ try {
55
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
56
+ if (pkg.name === ADK_PACKAGE) {
57
+ const root = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports["."] : undefined;
58
+ const target = typeof root === "object" && root !== null ? root.import : undefined;
59
+ return typeof target === "string" ? join(dir, target) : undefined;
60
+ }
61
+ }
62
+ catch {
63
+ // not at this level — keep walking.
64
+ }
65
+ const parent = dirname(dir);
66
+ if (parent === dir)
67
+ break;
68
+ dir = parent;
69
+ }
70
+ return undefined;
71
+ }
72
+ const ADK_PACKAGE = "@google/adk";
73
+ const DEFAULT_MODEL = "gemini-3.5-flash";
74
+ /**
75
+ * Load `@google/adk`, honoring the single-copy rule: when `entryPath` (the
76
+ * graceful agent module) is given, resolve the SDK from THAT tree so the
77
+ * runner drives the same copy the dev's agent was built with; otherwise
78
+ * import from the host's own tree. A resolution failure surfaces as the
79
+ * actionable missing-peer error.
80
+ */
81
+ export async function loadAdk(entryPath) {
82
+ try {
83
+ if (entryPath !== undefined) {
84
+ const entryRequire = createRequire(pathToFileURL(entryPath).href);
85
+ const resolved = entryRequire.resolve(ADK_PACKAGE);
86
+ // Dual-package hazard (review finding): `require.resolve` picks the
87
+ // exports REQUIRE condition (CJS build), but the dev's ESM agent module
88
+ // `import`s the IMPORT condition (ESM build) — same version, two module
89
+ // instances. Re-resolve the IMPORT-condition entry from the package
90
+ // root so the runner drives the SAME instance the dev's agent was
91
+ // built with; fall back to the require resolution when the package has
92
+ // no exports map (plain `main`).
93
+ return nativeLoad(importConditionEntry(resolved) ?? resolved);
94
+ }
95
+ return (await import(ADK_PACKAGE));
96
+ }
97
+ catch (err) {
98
+ const code = err.code;
99
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
100
+ throw new Error(`@guuey/host: cannot load the "${ADK_FRAMEWORK}" runner — its runtime is an optional peer. ` +
101
+ `Install ${ADK_PACKAGE} ${entryPath !== undefined ? `next to the agent entry (${entryPath})` : "next to @guuey/host"} ` +
102
+ `to run this framework. (${String(err)})`);
103
+ }
104
+ throw err;
105
+ }
106
+ }
107
+ /**
108
+ * Map the broker's credential files to ADK MCP toolsets. Throws on an `sse`
109
+ * credential — the JS toolset has no SSE transport (unlike the Python-era
110
+ * host); the error names the server and the supported path.
111
+ */
112
+ export function buildToolsets(adk, creds) {
113
+ return creds.map(({ name, cred }) => {
114
+ if (cred.transport === "sse") {
115
+ throw new Error(`@guuey/host: MCP server "${name}" uses transport "sse", which @google/adk's MCPToolset does not support ` +
116
+ `(Streamable-HTTP only). Point the server at a Streamable-HTTP endpoint or use a different framework for this agent.`);
117
+ }
118
+ return new adk.MCPToolset({
119
+ type: "StreamableHTTPConnectionParams",
120
+ url: cred.url,
121
+ transportOptions: { requestInit: { headers: cred.headers } },
122
+ });
123
+ });
124
+ }
125
+ /**
126
+ * Extract a native event's last non-thought text part ("" when none).
127
+ * Structural narrowing from `JsonValue` — the event stays untyped JSON on its
128
+ * way to the normalizer; only this thin slice is inspected.
129
+ */
130
+ export function finalTextOf(event) {
131
+ let finalText = "";
132
+ if (typeof event !== "object" || event === null || Array.isArray(event))
133
+ return finalText;
134
+ const content = event.content;
135
+ if (typeof content !== "object" || content === null || Array.isArray(content))
136
+ return finalText;
137
+ const parts = content.parts;
138
+ if (!Array.isArray(parts))
139
+ return finalText;
140
+ for (const part of parts) {
141
+ if (typeof part !== "object" || part === null || Array.isArray(part))
142
+ continue;
143
+ const { text, thought } = part;
144
+ if (typeof text === "string" && text !== "" && thought !== true)
145
+ finalText = text;
146
+ }
147
+ return finalText;
148
+ }
149
+ /**
150
+ * One turn against a caller-supplied agent + module (the seam graceful mode
151
+ * (T3) and the unit tests share). Emits hello → native* → done|error.
152
+ */
153
+ export async function runAdkTurn(adk, agent, turn, emit, sdkVersion) {
154
+ emit.hello(ADK_FRAMEWORK, ADK_PACKAGE, sdkVersion);
155
+ try {
156
+ const runner = new adk.InMemoryRunner({ agent });
157
+ const session = await runner.sessionService.createSession({
158
+ appName: runner.appName,
159
+ userId: turn.identity.userId,
160
+ });
161
+ let finalText = "";
162
+ for await (const event of runner.runAsync({
163
+ userId: turn.identity.userId,
164
+ sessionId: session.id,
165
+ // `role` pinned — see the module header (adk-js#475).
166
+ newMessage: { role: "user", parts: [{ text: turn.input }] },
167
+ // SSE streaming, matching the Python host's RunConfig(streaming_mode=SSE)
168
+ // — the ADK default is NONE, which silently drops incremental text
169
+ // (review finding). "sse" is StreamingMode.SSE's literal value.
170
+ runConfig: { streamingMode: "sse" },
171
+ })) {
172
+ // The full native event passes to the normalizer untouched; JS events
173
+ // are already the camelCase shapes the AdkEvent contract expects.
174
+ emit.native(ADK_FRAMEWORK, event);
175
+ finalText = finalTextOf(event) || finalText;
176
+ }
177
+ emit.done(finalText, "end_turn");
178
+ }
179
+ catch (err) {
180
+ // never propagate to the wire
181
+ emit.error(err instanceof Error ? `${err.name}: ${err.message}` : String(err));
182
+ }
183
+ }
184
+ /**
185
+ * Assemble the per-turn {@link GuueyContext} — the ONE discoverable object a
186
+ * graceful factory receives (spec §2.2). `instruction` carries the standard
187
+ * context preamble already prepended; the raw fields ride alongside for
188
+ * factories that render context themselves.
189
+ */
190
+ export function buildGuueyContext(snapshot, turn, instruction, mcpToolsets) {
191
+ return {
192
+ model: snapshot.model ?? DEFAULT_MODEL,
193
+ instruction,
194
+ mcpToolsets,
195
+ user: { id: turn.identity.userId, authMode: turn.identity.authMode },
196
+ files: { app: turn.fs.app, home: turn.fs.home, session: turn.fs.session },
197
+ history: turn.history.map((m) => ({ role: m.role, text: m.text })),
198
+ memory: (turn.priorMemory ?? []).map((m) => (m.key !== undefined ? { key: m.key, value: m.value } : { value: m.value })),
199
+ workingState: turn.priorState,
200
+ };
201
+ }
202
+ export function createRunner(deps = {}) {
203
+ const load = deps.load ?? loadAdk;
204
+ // Graceful mode: guuey.json#agent.entry → GUUEY_AGENT_ENTRY (relative),
205
+ // resolved strictly under the worker root. Read once at runner creation —
206
+ // the pod runs one agent module for its whole life.
207
+ const entryRel = process.env[AGENT_ENTRY_ENV];
208
+ const workerRoot = process.env[WORKER_ROOT_ENV];
209
+ let boot;
210
+ return {
211
+ async runTurn(snapshot, turn, emit) {
212
+ boot ??= (async () => {
213
+ if (entryRel !== undefined && entryRel !== "") {
214
+ const entryPath = resolveAgentEntry(entryRel, workerRoot);
215
+ // Single-copy rule: the SDK comes from the AGENT's own tree.
216
+ const adk = await load(entryPath);
217
+ const exported = await loadAgentEntry(entryPath);
218
+ return { adk, exported, entryPath };
219
+ }
220
+ return { adk: await load(), exported: undefined };
221
+ })();
222
+ let adk;
223
+ let exported;
224
+ let entryPath;
225
+ try {
226
+ ({ adk, exported, entryPath } = await boot);
227
+ }
228
+ catch (err) {
229
+ emit.hello(ADK_FRAMEWORK, ADK_PACKAGE, null);
230
+ emit.error(err instanceof Error ? err.message : String(err));
231
+ return;
232
+ }
233
+ const sdkVersion = resolveSdkVersion(ADK_PACKAGE, entryPath);
234
+ // Snapshots reach the worker fully resolved — a `{file}` systemPrompt
235
+ // here means an un-resolved snapshot hit the API directly; reject loudly
236
+ // (same posture as the claude path).
237
+ if (snapshot.systemPrompt !== undefined && typeof snapshot.systemPrompt !== "string") {
238
+ emit.hello(ADK_FRAMEWORK, ADK_PACKAGE, sdkVersion);
239
+ emit.error(`@guuey/host: snapshot.systemPrompt must be a resolved string (got ${JSON.stringify(snapshot.systemPrompt)}).`);
240
+ return;
241
+ }
242
+ const instruction = withContextPreamble(snapshot.systemPrompt ?? "", turn.history, turn.priorMemory, turn.priorState);
243
+ let agent;
244
+ try {
245
+ const toolsets = buildToolsets(adk, listCredentials(turn.fs)());
246
+ if (exported !== undefined) {
247
+ // Graceful: the dev's export (plain agent or factory(GuueyContext)).
248
+ const ctx = buildGuueyContext(snapshot, turn, instruction, toolsets);
249
+ agent = await materializeAgent(exported, ctx, (message) => process.stderr.write(`${message}\n`));
250
+ }
251
+ else {
252
+ // No-code: construct from the snapshot.
253
+ agent = new adk.LlmAgent({
254
+ name: "guuey",
255
+ model: snapshot.model ?? DEFAULT_MODEL,
256
+ instruction,
257
+ tools: toolsets,
258
+ });
259
+ }
260
+ }
261
+ catch (err) {
262
+ emit.hello(ADK_FRAMEWORK, ADK_PACKAGE, sdkVersion);
263
+ emit.error(err instanceof Error ? `${err.name}: ${err.message}` : String(err));
264
+ return;
265
+ }
266
+ await runAdkTurn(adk, agent, turn, emit, sdkVersion);
267
+ },
268
+ };
269
+ }
@@ -0,0 +1,3 @@
1
+ import type { FrameworkRunner } from "../index.js";
2
+ export declare function createRunner(): FrameworkRunner;
3
+ //# sourceMappingURL=openai-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-runner.d.ts","sourceRoot":"","sources":["../../src/frameworks/openai-runner.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,aAAa,CAAC;AAgB3E,wBAAgB,YAAY,IAAI,eAAe,CAW9C"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenAI runner module — the `FrameworkRunner` adapter around the existing
3
+ * openai turn loop (`openai.ts#runInvokeOpenai`). Imports the SDK at module
4
+ * top level so the host's lazy `import()` of this file is what pulls the
5
+ * optional peer.
6
+ *
7
+ * Credential posture (unchanged from the pre-restructure host): the key (or
8
+ * opaque broker token in hosted mode) is applied globally to the SDK via
9
+ * `setDefaultOpenAIKey` once at runner creation. The OpenAI SDK also reads
10
+ * `OPENAI_BASE_URL` + `OPENAI_API_KEY` from env directly, so the broker env
11
+ * injected by the Router's `buildWorkerEnv` already routes it.
12
+ */
13
+ import { run as openaiRun, setDefaultOpenAIKey } from "@openai/agents";
14
+ import { runInvokeOpenai } from "./openai.js";
15
+ import { listCredentials } from "../creds.js";
16
+ import { buildHostContext } from "../boot-context.js";
17
+ /**
18
+ * The real `@openai/agents` `run` (streamed overload), narrowed to the
19
+ * injected {@link OpenaiRunFn} surface the loop consumes. The SDK's `run` is
20
+ * generic over the agent + context; the loop only needs
21
+ * `(agent, input, {stream,maxTurns}) → a streamed result`. A typed adapter
22
+ * (NOT a cast) pins the streamed overload.
23
+ */
24
+ const realOpenaiRun = (agent, input, options) => openaiRun(agent, input, { stream: true, ...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}) });
25
+ export function createRunner() {
26
+ const bootCtx = buildHostContext(process.env);
27
+ if (bootCtx.openaiKey !== undefined)
28
+ setDefaultOpenAIKey(bootCtx.openaiKey);
29
+ return {
30
+ async runTurn(snapshot, turn, emit) {
31
+ // Broker fields (`baseUrl`/`authToken`) are only wired for the Claude
32
+ // path — the OpenAI SDK reads its env directly.
33
+ const runtime = { listCredentials: listCredentials(turn.fs) };
34
+ await runInvokeOpenai(snapshot, turn, runtime, emit, realOpenaiRun);
35
+ },
36
+ };
37
+ }
@@ -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 "./claude.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=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/frameworks/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,aAAa,CAAC;AAC3D,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 "./claude-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
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { type Emitter, type Invoke } from "@guuey/worker";
3
+ import type { GuueyAgent } from "@guuey/config";
4
+ /** The snapshot shape the host boots from (`framework` selects the runner). */
5
+ export type HostSnapshot = GuueyAgent & {
6
+ framework?: string;
7
+ };
8
+ /**
9
+ * One turn's input, as a runner receives it — the `Invoke` control message
10
+ * minus the discriminator.
11
+ */
12
+ export type HostTurn = Omit<Invoke, "type">;
13
+ /** The uniform surface every framework runner module exposes. */
14
+ export interface FrameworkRunner {
15
+ /** Run one turn: drive the SDK, emit native events; resolve when the turn ends. */
16
+ runTurn(snapshot: HostSnapshot, turn: HostTurn, emit: Emitter): Promise<void>;
17
+ }
18
+ /**
19
+ * Graceful mode (`GUUEY_AGENT_ENTRY`) is per-runner: a framework whose runner
20
+ * ignores the entry env must FAIL LOUDLY, not silently run the no-code
21
+ * snapshot instead of the dev's module (review finding — "non-goal" means
22
+ * rejected, not ignored). Boot-time check, same posture as a missing peer.
23
+ */
24
+ export declare function assertGracefulSupport(framework: string, agentEntryEnv: string | undefined): void;
25
+ /**
26
+ * Load the runner for `framework`, translating a module-resolution failure
27
+ * into the actionable missing-peer message (the runtimes are optional peers —
28
+ * the host deliberately does not bundle them).
29
+ */
30
+ export declare function loadRunner(framework: string): Promise<FrameworkRunner>;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA6BA,OAAO,EAKL,KAAK,OAAO,EACZ,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,OAAO,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAYD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAahG;AAYD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAoB5E"}