@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,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 @@
|
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|