rhachet-brains-openai 0.2.0 → 0.3.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.
@@ -1,4 +1,4 @@
1
- import { BrainAtom } from 'rhachet';
1
+ import { BrainAtom } from 'rhachet/brains';
2
2
  import { type OpenaiBrainAtomSlug } from '../../domain.objects/BrainAtom.config';
3
3
  /**
4
4
  * .what = factory to generate openai brain atom instances
@@ -1,4 +1,4 @@
1
- import { BrainRepl } from 'rhachet';
1
+ import { BrainRepl } from 'rhachet/brains';
2
2
  /**
3
3
  * .what = supported openai brain repl slugs
4
4
  * .why = enables type-safe slug specification with model variants
package/dist/index.js CHANGED
@@ -39,9 +39,7 @@ module.exports = __toCommonJS(index_exports);
39
39
 
40
40
  // src/domain.operations/atoms/genBrainAtom.ts
41
41
  var import_openai = __toESM(require("openai"));
42
- var import_rhachet = require("rhachet");
43
- var import_calcBrainOutputCost = require("rhachet/dist/domain.operations/brainCost/calcBrainOutputCost");
44
- var import_zod = require("zod");
42
+ var import_brains = require("rhachet/brains");
45
43
 
46
44
  // src/domain.objects/BrainAtom.config.ts
47
45
  var import_iso_price = require("iso-price");
@@ -803,33 +801,109 @@ var CONFIG_BY_ATOM_SLUG = {
803
801
  }
804
802
  };
805
803
 
804
+ // src/infra/cast/castFromOpenaiFunctionCall.ts
805
+ var castFromOpenaiFunctionCall = (input) => ({
806
+ exid: input.item.call_id,
807
+ slug: input.item.name,
808
+ input: JSON.parse(input.item.arguments)
809
+ });
810
+
811
+ // src/infra/cast/castIntoOpenaiFunctionCallOutput.ts
812
+ var genFunctionCallOutputId = () => `fc_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
813
+ var castIntoOpenaiFunctionCallOutput = (input) => ({
814
+ type: "function_call_output",
815
+ id: genFunctionCallOutputId(),
816
+ call_id: input.execution.exid,
817
+ output: JSON.stringify(input.execution.output)
818
+ });
819
+
820
+ // src/infra/schema/asJsonSchema.ts
821
+ var import_zod = require("zod");
822
+ var asJsonSchema = (input) => {
823
+ return import_zod.z.toJSONSchema(input.schema, { target: "openAi" });
824
+ };
825
+
826
+ // src/infra/cast/castIntoOpenaiFunctionTool.ts
827
+ var asOpenaiFunctionName = (slug) => slug.replace(/\./g, "_");
828
+ var castIntoOpenaiFunctionTool = (input) => ({
829
+ type: "function",
830
+ name: asOpenaiFunctionName(input.definition.slug),
831
+ description: input.definition.description,
832
+ parameters: asJsonSchema({ schema: input.definition.schema.input }),
833
+ strict: true
834
+ });
835
+
806
836
  // src/domain.operations/atoms/genBrainAtom.ts
837
+ var reconstructAssistantItems = (exchangeOutput) => {
838
+ try {
839
+ const parsed = JSON.parse(exchangeOutput);
840
+ if (Array.isArray(parsed)) {
841
+ const hasResponseItems = parsed.some(
842
+ (item) => item?.type === "function_call" || item?.type === "message" || item?.type === "reasoning"
843
+ );
844
+ if (hasResponseItems) {
845
+ return parsed;
846
+ }
847
+ }
848
+ } catch (error) {
849
+ if (!(error instanceof SyntaxError)) throw error;
850
+ }
851
+ return [{ role: "assistant", content: exchangeOutput }];
852
+ };
807
853
  var genBrainAtom = (input) => {
808
854
  const config = CONFIG_BY_ATOM_SLUG[input.slug];
809
- return new import_rhachet.BrainAtom({
855
+ return new import_brains.BrainAtom({
810
856
  repo: "openai",
811
857
  slug: input.slug,
812
858
  description: config.description,
813
859
  spec: config.spec,
814
860
  /**
815
- * .what = stateless inference (no tool use)
816
- * .why = provides direct model access for reason tasks
861
+ * .what = stateless inference with optional tool use
862
+ * .why = provides direct model access for reason tasks, supports tool invocations
817
863
  */
818
864
  ask: async (askInput, context) => {
819
865
  const startTime = Date.now();
820
- const systemPrompt = askInput.role.briefs ? await (0, import_rhachet.castBriefsToPrompt)({ briefs: askInput.role.briefs }) : void 0;
866
+ const systemPrompt = askInput.role.briefs ? await (0, import_brains.castBriefsToPrompt)({ briefs: askInput.role.briefs }) : void 0;
821
867
  const openai = context?.openai ?? new import_openai.default({ apiKey: process.env.OPENAI_API_KEY });
822
- const jsonSchema = import_zod.z.toJSONSchema(askInput.schema.output);
868
+ const jsonSchema = asJsonSchema({ schema: askInput.schema.output });
823
869
  const isObjectSchema = typeof jsonSchema === "object" && jsonSchema !== null && "type" in jsonSchema && jsonSchema.type === "object";
824
- const fullPrompt = systemPrompt ? `${systemPrompt}
870
+ const isToolExecutionArray = Array.isArray(askInput.prompt);
871
+ const pluggedTools = askInput.plugs?.tools ?? [];
872
+ const hasTools = pluggedTools.length > 0;
873
+ const fullPrompt = isToolExecutionArray ? null : systemPrompt ? `${systemPrompt}
825
874
 
826
875
  ---
827
876
 
828
877
  ${askInput.prompt}` : askInput.prompt;
878
+ const openaiFunctionTools = hasTools ? pluggedTools.map(
879
+ (definition) => castIntoOpenaiFunctionTool({ definition })
880
+ ) : [];
881
+ const priorMessages = askInput.on?.episode?.exchanges.flatMap((exchange) => [
882
+ { role: "user", content: exchange.input },
883
+ ...reconstructAssistantItems(exchange.output)
884
+ ]) ?? [];
885
+ const currentInput = isToolExecutionArray ? askInput.prompt.map(
886
+ (execution) => castIntoOpenaiFunctionCallOutput({ execution })
887
+ ) : [{ role: "user", content: fullPrompt }];
888
+ const systemInstruction = hasTools && isObjectSchema ? [
889
+ {
890
+ role: "system",
891
+ content: `When you have the final answer, respond with valid JSON matching this schema:
892
+ ${JSON.stringify(jsonSchema, null, 2)}`
893
+ }
894
+ ] : [];
895
+ const inputMessages = [
896
+ ...systemInstruction,
897
+ ...priorMessages,
898
+ ...currentInput
899
+ ];
829
900
  const response = await openai.responses.create({
830
901
  model: config.model,
831
- input: fullPrompt,
832
- ...isObjectSchema && {
902
+ input: inputMessages,
903
+ ...hasTools && {
904
+ tools: openaiFunctionTools
905
+ },
906
+ ...!hasTools && isObjectSchema && {
833
907
  text: {
834
908
  format: {
835
909
  type: "json_schema",
@@ -840,6 +914,14 @@ ${askInput.prompt}` : askInput.prompt;
840
914
  }
841
915
  }
842
916
  });
917
+ const functionCallItems = response.output.filter(
918
+ (item) => item.type === "function_call"
919
+ );
920
+ const calls = functionCallItems.length > 0 ? {
921
+ tools: functionCallItems.map(
922
+ (item) => castFromOpenaiFunctionCall({ item })
923
+ )
924
+ } : null;
843
925
  const outputItem = response.output.find(
844
926
  (item) => item.type === "message"
845
927
  );
@@ -849,7 +931,7 @@ ${askInput.prompt}` : askInput.prompt;
849
931
  const tokensOutput = response.usage?.output_tokens ?? 0;
850
932
  const tokensCacheGet = response.usage?.input_tokens_details?.cached_tokens ?? 0;
851
933
  const elapsedMs = Date.now() - startTime;
852
- const charsInput = fullPrompt.length;
934
+ const charsInput = isToolExecutionArray ? JSON.stringify(askInput.prompt).length : fullPrompt.length;
853
935
  const charsOutput = content.length;
854
936
  const size = {
855
937
  tokens: {
@@ -863,19 +945,70 @@ ${askInput.prompt}` : askInput.prompt;
863
945
  cache: { get: 0, set: 0 }
864
946
  }
865
947
  };
866
- const { cash } = (0, import_calcBrainOutputCost.calcBrainOutputCost)({
948
+ const { cash } = (0, import_brains.calcBrainOutputCost)({
867
949
  for: { tokens: size.tokens },
868
950
  with: { cost: { cash: config.spec.cost.cash } }
869
951
  });
870
- const metrics = new import_rhachet.BrainOutputMetrics({
952
+ const metrics = new import_brains.BrainOutputMetrics({
871
953
  size,
872
954
  cost: {
873
955
  time: { milliseconds: elapsedMs },
874
956
  cash
875
957
  }
876
958
  });
877
- const output = isObjectSchema ? askInput.schema.output.parse(JSON.parse(content)) : askInput.schema.output.parse(content);
878
- return new import_rhachet.BrainOutput({ output, metrics });
959
+ const output = (() => {
960
+ if (!content) return null;
961
+ if (hasTools && isObjectSchema) {
962
+ try {
963
+ return askInput.schema.output.parse(JSON.parse(content));
964
+ } catch (error) {
965
+ const isParseError = error instanceof SyntaxError || error?.constructor?.name === "ZodError";
966
+ if (!isParseError) throw error;
967
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
968
+ if (jsonMatch) {
969
+ try {
970
+ return askInput.schema.output.parse(JSON.parse(jsonMatch[0]));
971
+ } catch (extractError) {
972
+ const isExtractParseError = extractError instanceof SyntaxError || extractError?.constructor?.name === "ZodError";
973
+ if (!isExtractParseError) throw extractError;
974
+ }
975
+ }
976
+ if (calls) return null;
977
+ return null;
978
+ }
979
+ }
980
+ try {
981
+ return isObjectSchema ? askInput.schema.output.parse(JSON.parse(content)) : askInput.schema.output.parse(content);
982
+ } catch (error) {
983
+ const isParseError = error instanceof SyntaxError || error?.constructor?.name === "ZodError";
984
+ if (!isParseError) throw error;
985
+ if (calls) return null;
986
+ throw new Error("structured output parse failed with no tool calls");
987
+ }
988
+ })();
989
+ const exchangeInput = isToolExecutionArray ? JSON.stringify(askInput.prompt) : askInput.prompt;
990
+ const exchangeOutput = functionCallItems.length > 0 ? JSON.stringify(response.output) : content;
991
+ const continuables = await (0, import_brains.genBrainContinuables)({
992
+ for: { grain: "atom" },
993
+ on: {
994
+ episode: askInput.on?.episode ?? null,
995
+ series: null
996
+ },
997
+ with: {
998
+ exchange: {
999
+ input: exchangeInput,
1000
+ output: exchangeOutput,
1001
+ exid: response.id
1002
+ }
1003
+ }
1004
+ });
1005
+ return new import_brains.BrainOutput({
1006
+ output,
1007
+ calls,
1008
+ metrics,
1009
+ episode: continuables.episode,
1010
+ series: continuables.series
1011
+ });
879
1012
  }
880
1013
  });
881
1014
  };
@@ -1235,17 +1368,9 @@ var Codex = class {
1235
1368
  };
1236
1369
 
1237
1370
  // src/domain.operations/repls/genBrainRepl.ts
1238
- var import_rhachet2 = require("rhachet");
1239
- var import_calcBrainOutputCost2 = require("rhachet/dist/domain.operations/brainCost/calcBrainOutputCost");
1371
+ var import_helpful_errors = require("helpful-errors");
1372
+ var import_brains2 = require("rhachet/brains");
1240
1373
  var import_wrapper_fns = require("wrapper-fns");
1241
-
1242
- // src/infra/schema/asJsonSchema.ts
1243
- var import_zod2 = require("zod");
1244
- var asJsonSchema = (input) => {
1245
- return import_zod2.z.toJSONSchema(input.schema, { target: "openAi" });
1246
- };
1247
-
1248
- // src/domain.operations/repls/genBrainRepl.ts
1249
1374
  var CONFIG_BY_REPL_SLUG = {
1250
1375
  // default
1251
1376
  "openai/codex": CONFIG_BY_ATOM_SLUG["openai/gpt/codex/5.1-max"],
@@ -1267,9 +1392,17 @@ var composePromptWithSystem = (userPrompt, systemPrompt) => {
1267
1392
 
1268
1393
  ${userPrompt}`;
1269
1394
  };
1395
+ var EXID_PREFIX = "openai/codex";
1396
+ var encodeExid = (threadId) => `${EXID_PREFIX}/${threadId}`;
1397
+ var decodeExid = (exid) => {
1398
+ if (!exid.startsWith(`${EXID_PREFIX}/`)) return { valid: false };
1399
+ const threadId = exid.slice(`${EXID_PREFIX}/`.length);
1400
+ if (!threadId) return { valid: false };
1401
+ return { valid: true, threadId };
1402
+ };
1270
1403
  var invokeCodex = async (input) => {
1271
1404
  const startTime = Date.now();
1272
- const systemPrompt = input.role.briefs ? await (0, import_rhachet2.castBriefsToPrompt)({ briefs: input.role.briefs }) : void 0;
1405
+ const systemPrompt = input.role.briefs ? await (0, import_brains2.castBriefsToPrompt)({ briefs: input.role.briefs }) : void 0;
1273
1406
  const outputSchema = asJsonSchema({
1274
1407
  schema: input.schema.output
1275
1408
  });
@@ -1277,10 +1410,18 @@ var invokeCodex = async (input) => {
1277
1410
  apiKey: process.env.OPENAI_API_KEY
1278
1411
  });
1279
1412
  const sandboxMode = input.mode === "ask" ? "read-only" : "workspace-write";
1280
- const thread = codex.startThread({
1281
- model: input.model,
1282
- sandboxMode
1283
- });
1413
+ const threadOptions = { model: input.model, sandboxMode };
1414
+ const priorExid = input.on?.episode?.exid ?? null;
1415
+ const thread = (() => {
1416
+ if (!priorExid) return codex.startThread(threadOptions);
1417
+ const decoded = decodeExid(priorExid);
1418
+ if (!decoded.valid)
1419
+ throw new import_helpful_errors.BadRequestError(
1420
+ "episode continuation failed: exid is not from openai/codex. cross-supplier continuation is not supported.",
1421
+ { priorExid }
1422
+ );
1423
+ return codex.resumeThread(decoded.threadId, threadOptions);
1424
+ })();
1284
1425
  const fullPrompt = composePromptWithSystem(input.prompt, systemPrompt);
1285
1426
  const response = await (0, import_wrapper_fns.withRetry)(
1286
1427
  (0, import_wrapper_fns.withTimeout)(async () => thread.run(fullPrompt, { outputSchema }), {
@@ -1313,46 +1454,71 @@ var invokeCodex = async (input) => {
1313
1454
  cache: { get: 0, set: 0 }
1314
1455
  }
1315
1456
  };
1316
- const { cash } = (0, import_calcBrainOutputCost2.calcBrainOutputCost)({
1457
+ const { cash } = (0, import_brains2.calcBrainOutputCost)({
1317
1458
  for: { tokens: size.tokens },
1318
1459
  with: { cost: { cash: input.spec.cost.cash } }
1319
1460
  });
1320
- const metrics = new import_rhachet2.BrainOutputMetrics({
1461
+ const metrics = new import_brains2.BrainOutputMetrics({
1321
1462
  size,
1322
1463
  cost: {
1323
1464
  time: { milliseconds: elapsedMs },
1324
1465
  cash
1325
1466
  }
1326
1467
  });
1327
- return new import_rhachet2.BrainOutput({ output, metrics });
1468
+ const threadExid = thread.id ? encodeExid(thread.id) : null;
1469
+ const continuables = await (0, import_brains2.genBrainContinuables)({
1470
+ for: { grain: "repl" },
1471
+ on: {
1472
+ episode: input.on?.episode ?? null,
1473
+ series: input.on?.series ?? null
1474
+ },
1475
+ with: {
1476
+ exchange: {
1477
+ input: input.prompt,
1478
+ output: content,
1479
+ exid: thread.id
1480
+ },
1481
+ episode: { exid: threadExid }
1482
+ }
1483
+ });
1484
+ return new import_brains2.BrainOutput({
1485
+ output,
1486
+ calls: null,
1487
+ metrics,
1488
+ episode: continuables.episode,
1489
+ series: continuables.series
1490
+ });
1328
1491
  };
1329
1492
  var genBrainRepl = (input) => {
1330
1493
  const config = CONFIG_BY_REPL_SLUG[input.slug];
1331
- return new import_rhachet2.BrainRepl({
1494
+ return new import_brains2.BrainRepl({
1332
1495
  repo: "openai",
1333
1496
  slug: input.slug,
1334
1497
  description: config.description,
1335
1498
  spec: config.spec,
1336
- /**
1337
- * .what = readonly analysis (research, queries, code review)
1338
- * .why = provides safe, non-mutate agent interactions via read-only sandbox
1339
- */
1340
- ask: async (askInput, _context) => invokeCodex({
1499
+ // note: repls only accept string prompts (tool execution handled internally by codex SDK)
1500
+ // type assertions needed because BrainRepl contract now supports AsBrainPromptFor<TPlugs>
1501
+ // but codex SDK's thread.run() only accepts string prompts
1502
+ ask: (async (askInput, _context) => invokeCodex({
1341
1503
  mode: "ask",
1342
1504
  model: config.model,
1343
1505
  spec: config.spec,
1344
- ...askInput
1345
- }),
1346
- /**
1347
- * .what = read+write actions (code changes, file edits)
1348
- * .why = provides full agentic capabilities via workspace-write sandbox
1349
- */
1350
- act: async (actInput, _context) => invokeCodex({
1506
+ on: askInput.on,
1507
+ plugs: askInput.plugs,
1508
+ role: askInput.role,
1509
+ prompt: askInput.prompt,
1510
+ schema: askInput.schema
1511
+ })),
1512
+ act: (async (actInput, _context) => invokeCodex({
1351
1513
  mode: "act",
1352
1514
  model: config.model,
1353
1515
  spec: config.spec,
1354
- ...actInput
1355
- })
1516
+ on: actInput.on,
1517
+ plugs: actInput.plugs,
1518
+ role: actInput.role,
1519
+ prompt: actInput.prompt,
1520
+ schema: actInput.schema
1521
+ }))
1356
1522
  });
1357
1523
  };
1358
1524
 
@@ -0,0 +1,9 @@
1
+ import type OpenAI from 'openai';
2
+ import type { BrainPlugToolInvocation } from 'rhachet/brains';
3
+ /**
4
+ * .what = cast openai function_call to rhachet tool invocation
5
+ * .why = explicit boundary between openai sdk and rhachet domain
6
+ */
7
+ export declare const castFromOpenaiFunctionCall: (input: {
8
+ item: OpenAI.Responses.ResponseFunctionToolCall;
9
+ }) => BrainPlugToolInvocation<unknown>;
@@ -0,0 +1,12 @@
1
+ import type OpenAI from 'openai';
2
+ import type { BrainPlugToolExecution } from 'rhachet/brains';
3
+ /**
4
+ * .what = cast rhachet tool execution to openai function_call_output format
5
+ * .why = explicit boundary between rhachet domain and openai sdk
6
+ *
7
+ * .note = id is a new generated id for this output item (must start with fc_)
8
+ * .note = call_id references the original function_call this responds to
9
+ */
10
+ export declare const castIntoOpenaiFunctionCallOutput: (input: {
11
+ execution: BrainPlugToolExecution<unknown, unknown>;
12
+ }) => OpenAI.Responses.ResponseFunctionToolCallOutputItem;
@@ -0,0 +1,14 @@
1
+ import type OpenAI from 'openai';
2
+ import type { BrainPlugToolDefinition } from 'rhachet/brains';
3
+ /**
4
+ * .what = transforms slug to valid openai function name
5
+ * .why = openai requires names match pattern ^[a-zA-Z0-9_-]+$
6
+ */
7
+ export declare const asOpenaiFunctionName: (slug: string) => string;
8
+ /**
9
+ * .what = cast rhachet tool definition to openai function tool format
10
+ * .why = explicit boundary between rhachet domain and openai sdk
11
+ */
12
+ export declare const castIntoOpenaiFunctionTool: (input: {
13
+ definition: BrainPlugToolDefinition<unknown, unknown, 'atom', string>;
14
+ }) => OpenAI.Responses.FunctionTool;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "rhachet-brains-openai",
3
3
  "author": "ehmpathy",
4
4
  "description": "rhachet brain.atom and brain.repl adapter for openai",
5
- "version": "0.2.0",
5
+ "version": "0.3.0",
6
6
  "repository": "ehmpathy/rhachet-brains-openai",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-brains-openai",
8
8
  "keywords": [
@@ -54,7 +54,7 @@
54
54
  "preversion": "npm run prepush",
55
55
  "postversion": "git push origin HEAD --tags --no-verify",
56
56
  "prepare:husky": "husky install && chmod ug+x .husky/*",
57
- "prepare:rhachet": "rhachet init --mode upsert && rhachet roles link --repo ehmpathy --role mechanic && rhachet roles link --repo bhuild --role behaver && rhachet roles link --repo bhrain --role reviewer && rhachet roles init --role mechanic && rhachet roles init --role behaver",
57
+ "prepare:rhachet": "npm run build && rhachet init --hooks --roles behaver driver mechanic reviewer architect ergonomist librarian",
58
58
  "prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi"
59
59
  },
60
60
  "dependencies": {
@@ -90,10 +90,11 @@
90
90
  "husky": "8.0.3",
91
91
  "iso-time": "1.11.1",
92
92
  "jest": "30.2.0",
93
- "rhachet": "1.26.0",
94
- "rhachet-roles-bhrain": "0.5.9",
95
- "rhachet-roles-bhuild": "0.6.3",
96
- "rhachet-roles-ehmpathy": "1.17.15",
93
+ "rhachet": "1.37.17",
94
+ "rhachet-brains-anthropic": "0.3.3",
95
+ "rhachet-roles-bhrain": "0.20.0",
96
+ "rhachet-roles-bhuild": "0.14.2",
97
+ "rhachet-roles-ehmpathy": "1.29.0",
97
98
  "test-fns": "1.10.0",
98
99
  "tsc-alias": "1.8.10",
99
100
  "tsx": "4.20.6",
package/readme.md CHANGED
@@ -51,6 +51,60 @@ const { output: { proposal } } = await brainRepl.act({
51
51
  });
52
52
  ```
53
53
 
54
+ ## continuation support
55
+
56
+ both atoms and repls support multi-turn conversations via episode continuation.
57
+
58
+ ### atoms
59
+
60
+ atoms support continuation via the openai responses api. pass the prior episode to continue the conversation:
61
+
62
+ ```ts
63
+ // first call establishes context
64
+ const resultFirst = await brainAtom.ask({
65
+ role: {},
66
+ prompt: 'remember the secret word "PINEAPPLE42"',
67
+ schema: { output: z.object({ content: z.string() }) },
68
+ });
69
+
70
+ // second call continues with prior context
71
+ const resultSecond = await brainAtom.ask({
72
+ on: { episode: resultFirst.episode },
73
+ role: {},
74
+ prompt: 'what is the secret word I told you?',
75
+ schema: { output: z.object({ content: z.string() }) },
76
+ });
77
+ // resultSecond.output.content contains "PINEAPPLE42"
78
+ ```
79
+
80
+ ### repls
81
+
82
+ repls support continuation via the codex-sdk `resumeThread()` api. the episode exid contains the thread id prefixed with `openai/codex/` for cross-supplier validation:
83
+
84
+ ```ts
85
+ // first call starts a new thread
86
+ const resultFirst = await brainRepl.ask({
87
+ role: {},
88
+ prompt: 'remember the secret word "MANGO99"',
89
+ schema: { output: z.object({ content: z.string() }) },
90
+ });
91
+ // resultFirst.episode.exid = "openai/codex/{threadId}"
92
+
93
+ // second call resumes the thread
94
+ const resultSecond = await brainRepl.ask({
95
+ on: { episode: resultFirst.episode },
96
+ role: {},
97
+ prompt: 'what is the secret word I told you?',
98
+ schema: { output: z.object({ content: z.string() }) },
99
+ });
100
+ // resultSecond.output.content contains "MANGO99"
101
+ ```
102
+
103
+ ### limitations
104
+
105
+ - **cross-supplier continuation is not supported**: episodes from other brain suppliers (e.g., anthropic) cannot be used to continue openai conversations. this throws a `BadRequestError`.
106
+ - **exid validation**: the episode exid must start with `openai/codex/` for repl continuation. this prevents accidental cross-supplier continuation attempts.
107
+
54
108
  ## available brains
55
109
 
56
110
  ### atoms (via genBrainAtom)