rhachet-brains-openai 0.2.1 → 0.3.1

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/dist/index.js CHANGED
@@ -40,7 +40,6 @@ module.exports = __toCommonJS(index_exports);
40
40
  // src/domain.operations/atoms/genBrainAtom.ts
41
41
  var import_openai = __toESM(require("openai"));
42
42
  var import_brains = require("rhachet/brains");
43
- var import_zod = require("zod");
44
43
 
45
44
  // src/domain.objects/BrainAtom.config.ts
46
45
  var import_iso_price = require("iso-price");
@@ -802,7 +801,88 @@ var CONFIG_BY_ATOM_SLUG = {
802
801
  }
803
802
  };
804
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 asOpenaiStrictSchema = (schema) => {
823
+ if (!schema.properties) return schema;
824
+ const requiredSet = new Set(schema.required ?? []);
825
+ const allPropertyNames = Object.keys(schema.properties);
826
+ const propertiesTransformed = {};
827
+ for (const [name, prop] of Object.entries(schema.properties)) {
828
+ const isOptional = !requiredSet.has(name);
829
+ let propTransformed = { ...prop };
830
+ if (prop.type === "object" && prop.properties) {
831
+ propTransformed = asOpenaiStrictSchema(prop);
832
+ }
833
+ if (isOptional && propTransformed.type) {
834
+ const currentType = propTransformed.type;
835
+ if (Array.isArray(currentType)) {
836
+ if (!currentType.includes("null")) {
837
+ propTransformed = {
838
+ ...propTransformed,
839
+ type: [...currentType, "null"]
840
+ };
841
+ }
842
+ } else {
843
+ propTransformed = { ...propTransformed, type: [currentType, "null"] };
844
+ }
845
+ }
846
+ propertiesTransformed[name] = propTransformed;
847
+ }
848
+ return {
849
+ ...schema,
850
+ properties: propertiesTransformed,
851
+ required: allPropertyNames
852
+ };
853
+ };
854
+ var asJsonSchema = (input) => {
855
+ const schema = import_zod.z.toJSONSchema(input.schema, { target: "openAi" });
856
+ return asOpenaiStrictSchema(schema);
857
+ };
858
+
859
+ // src/infra/cast/castIntoOpenaiFunctionTool.ts
860
+ var asOpenaiFunctionName = (slug) => slug.replace(/\./g, "_");
861
+ var castIntoOpenaiFunctionTool = (input) => ({
862
+ type: "function",
863
+ name: asOpenaiFunctionName(input.definition.slug),
864
+ description: input.definition.description,
865
+ parameters: asJsonSchema({ schema: input.definition.schema.input }),
866
+ strict: true
867
+ });
868
+
805
869
  // src/domain.operations/atoms/genBrainAtom.ts
870
+ var reconstructAssistantItems = (exchangeOutput) => {
871
+ try {
872
+ const parsed = JSON.parse(exchangeOutput);
873
+ if (Array.isArray(parsed)) {
874
+ const hasResponseItems = parsed.some(
875
+ (item) => item?.type === "function_call" || item?.type === "message" || item?.type === "reasoning"
876
+ );
877
+ if (hasResponseItems) {
878
+ return parsed;
879
+ }
880
+ }
881
+ } catch (error) {
882
+ if (!(error instanceof SyntaxError)) throw error;
883
+ }
884
+ return [{ role: "assistant", content: exchangeOutput }];
885
+ };
806
886
  var genBrainAtom = (input) => {
807
887
  const config = CONFIG_BY_ATOM_SLUG[input.slug];
808
888
  return new import_brains.BrainAtom({
@@ -811,32 +891,52 @@ var genBrainAtom = (input) => {
811
891
  description: config.description,
812
892
  spec: config.spec,
813
893
  /**
814
- * .what = stateless inference (no tool use)
815
- * .why = provides direct model access for reason tasks
894
+ * .what = stateless inference with optional tool use
895
+ * .why = provides direct model access for reason tasks, supports tool invocations
816
896
  */
817
897
  ask: async (askInput, context) => {
818
898
  const startTime = Date.now();
819
899
  const systemPrompt = askInput.role.briefs ? await (0, import_brains.castBriefsToPrompt)({ briefs: askInput.role.briefs }) : void 0;
820
900
  const openai = context?.openai ?? new import_openai.default({ apiKey: process.env.OPENAI_API_KEY });
821
- const jsonSchema = import_zod.z.toJSONSchema(askInput.schema.output);
901
+ const jsonSchema = asJsonSchema({ schema: askInput.schema.output });
822
902
  const isObjectSchema = typeof jsonSchema === "object" && jsonSchema !== null && "type" in jsonSchema && jsonSchema.type === "object";
823
- const fullPrompt = systemPrompt ? `${systemPrompt}
903
+ const isToolExecutionArray = Array.isArray(askInput.prompt);
904
+ const pluggedTools = askInput.plugs?.tools ?? [];
905
+ const hasTools = pluggedTools.length > 0;
906
+ const fullPrompt = isToolExecutionArray ? null : systemPrompt ? `${systemPrompt}
824
907
 
825
908
  ---
826
909
 
827
910
  ${askInput.prompt}` : askInput.prompt;
911
+ const openaiFunctionTools = hasTools ? pluggedTools.map(
912
+ (definition) => castIntoOpenaiFunctionTool({ definition })
913
+ ) : [];
828
914
  const priorMessages = askInput.on?.episode?.exchanges.flatMap((exchange) => [
829
915
  { role: "user", content: exchange.input },
830
- { role: "assistant", content: exchange.output }
916
+ ...reconstructAssistantItems(exchange.output)
831
917
  ]) ?? [];
918
+ const currentInput = isToolExecutionArray ? askInput.prompt.map(
919
+ (execution) => castIntoOpenaiFunctionCallOutput({ execution })
920
+ ) : [{ role: "user", content: fullPrompt }];
921
+ const systemInstruction = hasTools && isObjectSchema ? [
922
+ {
923
+ role: "system",
924
+ content: `When you have the final answer, respond with valid JSON matching this schema:
925
+ ${JSON.stringify(jsonSchema, null, 2)}`
926
+ }
927
+ ] : [];
832
928
  const inputMessages = [
929
+ ...systemInstruction,
833
930
  ...priorMessages,
834
- { role: "user", content: fullPrompt }
931
+ ...currentInput
835
932
  ];
836
933
  const response = await openai.responses.create({
837
934
  model: config.model,
838
935
  input: inputMessages,
839
- ...isObjectSchema && {
936
+ ...hasTools && {
937
+ tools: openaiFunctionTools
938
+ },
939
+ ...!hasTools && isObjectSchema && {
840
940
  text: {
841
941
  format: {
842
942
  type: "json_schema",
@@ -847,6 +947,14 @@ ${askInput.prompt}` : askInput.prompt;
847
947
  }
848
948
  }
849
949
  });
950
+ const functionCallItems = response.output.filter(
951
+ (item) => item.type === "function_call"
952
+ );
953
+ const calls = functionCallItems.length > 0 ? {
954
+ tools: functionCallItems.map(
955
+ (item) => castFromOpenaiFunctionCall({ item })
956
+ )
957
+ } : null;
850
958
  const outputItem = response.output.find(
851
959
  (item) => item.type === "message"
852
960
  );
@@ -856,7 +964,7 @@ ${askInput.prompt}` : askInput.prompt;
856
964
  const tokensOutput = response.usage?.output_tokens ?? 0;
857
965
  const tokensCacheGet = response.usage?.input_tokens_details?.cached_tokens ?? 0;
858
966
  const elapsedMs = Date.now() - startTime;
859
- const charsInput = fullPrompt.length;
967
+ const charsInput = isToolExecutionArray ? JSON.stringify(askInput.prompt).length : fullPrompt.length;
860
968
  const charsOutput = content.length;
861
969
  const size = {
862
970
  tokens: {
@@ -881,7 +989,38 @@ ${askInput.prompt}` : askInput.prompt;
881
989
  cash
882
990
  }
883
991
  });
884
- const output = isObjectSchema ? askInput.schema.output.parse(JSON.parse(content)) : askInput.schema.output.parse(content);
992
+ const output = (() => {
993
+ if (!content) return null;
994
+ if (hasTools && isObjectSchema) {
995
+ try {
996
+ return askInput.schema.output.parse(JSON.parse(content));
997
+ } catch (error) {
998
+ const isParseError = error instanceof SyntaxError || error?.constructor?.name === "ZodError";
999
+ if (!isParseError) throw error;
1000
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1001
+ if (jsonMatch) {
1002
+ try {
1003
+ return askInput.schema.output.parse(JSON.parse(jsonMatch[0]));
1004
+ } catch (extractError) {
1005
+ const isExtractParseError = extractError instanceof SyntaxError || extractError?.constructor?.name === "ZodError";
1006
+ if (!isExtractParseError) throw extractError;
1007
+ }
1008
+ }
1009
+ if (calls) return null;
1010
+ return null;
1011
+ }
1012
+ }
1013
+ try {
1014
+ return isObjectSchema ? askInput.schema.output.parse(JSON.parse(content)) : askInput.schema.output.parse(content);
1015
+ } catch (error) {
1016
+ const isParseError = error instanceof SyntaxError || error?.constructor?.name === "ZodError";
1017
+ if (!isParseError) throw error;
1018
+ if (calls) return null;
1019
+ throw new Error("structured output parse failed with no tool calls");
1020
+ }
1021
+ })();
1022
+ const exchangeInput = isToolExecutionArray ? JSON.stringify(askInput.prompt) : askInput.prompt;
1023
+ const exchangeOutput = functionCallItems.length > 0 ? JSON.stringify(response.output) : content;
885
1024
  const continuables = await (0, import_brains.genBrainContinuables)({
886
1025
  for: { grain: "atom" },
887
1026
  on: {
@@ -890,14 +1029,15 @@ ${askInput.prompt}` : askInput.prompt;
890
1029
  },
891
1030
  with: {
892
1031
  exchange: {
893
- input: askInput.prompt,
894
- output: content,
1032
+ input: exchangeInput,
1033
+ output: exchangeOutput,
895
1034
  exid: response.id
896
1035
  }
897
1036
  }
898
1037
  });
899
1038
  return new import_brains.BrainOutput({
900
1039
  output,
1040
+ calls,
901
1041
  metrics,
902
1042
  episode: continuables.episode,
903
1043
  series: continuables.series
@@ -1264,14 +1404,6 @@ var Codex = class {
1264
1404
  var import_helpful_errors = require("helpful-errors");
1265
1405
  var import_brains2 = require("rhachet/brains");
1266
1406
  var import_wrapper_fns = require("wrapper-fns");
1267
-
1268
- // src/infra/schema/asJsonSchema.ts
1269
- var import_zod2 = require("zod");
1270
- var asJsonSchema = (input) => {
1271
- return import_zod2.z.toJSONSchema(input.schema, { target: "openAi" });
1272
- };
1273
-
1274
- // src/domain.operations/repls/genBrainRepl.ts
1275
1407
  var CONFIG_BY_REPL_SLUG = {
1276
1408
  // default
1277
1409
  "openai/codex": CONFIG_BY_ATOM_SLUG["openai/gpt/codex/5.1-max"],
@@ -1384,6 +1516,7 @@ var invokeCodex = async (input) => {
1384
1516
  });
1385
1517
  return new import_brains2.BrainOutput({
1386
1518
  output,
1519
+ calls: null,
1387
1520
  metrics,
1388
1521
  episode: continuables.episode,
1389
1522
  series: continuables.series
@@ -1396,11 +1529,10 @@ var genBrainRepl = (input) => {
1396
1529
  slug: input.slug,
1397
1530
  description: config.description,
1398
1531
  spec: config.spec,
1399
- /**
1400
- * .what = readonly analysis (research, queries, code review)
1401
- * .why = provides safe, non-mutate agent interactions via read-only sandbox
1402
- */
1403
- ask: async (askInput, _context) => invokeCodex({
1532
+ // note: repls only accept string prompts (tool execution handled internally by codex SDK)
1533
+ // type assertions needed because BrainRepl contract now supports AsBrainPromptFor<TPlugs>
1534
+ // but codex SDK's thread.run() only accepts string prompts
1535
+ ask: (async (askInput, _context) => invokeCodex({
1404
1536
  mode: "ask",
1405
1537
  model: config.model,
1406
1538
  spec: config.spec,
@@ -1409,12 +1541,8 @@ var genBrainRepl = (input) => {
1409
1541
  role: askInput.role,
1410
1542
  prompt: askInput.prompt,
1411
1543
  schema: askInput.schema
1412
- }),
1413
- /**
1414
- * .what = read+write actions (code changes, file edits)
1415
- * .why = provides full agentic capabilities via workspace-write sandbox
1416
- */
1417
- act: async (actInput, _context) => invokeCodex({
1544
+ })),
1545
+ act: (async (actInput, _context) => invokeCodex({
1418
1546
  mode: "act",
1419
1547
  model: config.model,
1420
1548
  spec: config.spec,
@@ -1423,7 +1551,7 @@ var genBrainRepl = (input) => {
1423
1551
  role: actInput.role,
1424
1552
  prompt: actInput.prompt,
1425
1553
  schema: actInput.schema
1426
- })
1554
+ }))
1427
1555
  });
1428
1556
  };
1429
1557
 
@@ -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;
@@ -1,12 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  /**
3
3
  * .what = convert a zod schema to JSON schema for native SDK enforcement
4
- * .why = enables native structured output support in SDKs, reducing
4
+ * .why = enables native structured output support in SDKs to reduce
5
5
  * token waste on validation retries
6
6
  *
7
7
  * .note = different SDKs require different conversion options:
8
8
  * - claude-agent-sdk: { $refStrategy: 'root' }
9
9
  * - codex-sdk: { target: 'openAi' }
10
+ *
11
+ * .note = openai strict mode requires all properties in required array
12
+ * with optional properties marked as nullable types
10
13
  */
11
14
  export declare const asJsonSchema: (input: {
12
15
  schema: z.ZodSchema;
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.1",
5
+ "version": "0.3.1",
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 --hooks --roles behaver mechanic reviewer",
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,11 +90,11 @@
90
90
  "husky": "8.0.3",
91
91
  "iso-time": "1.11.1",
92
92
  "jest": "30.2.0",
93
- "rhachet": "1.29.5",
94
- "rhachet-brains-anthropic": "0.3.2",
95
- "rhachet-roles-bhrain": "0.7.5",
96
- "rhachet-roles-bhuild": "0.7.0",
97
- "rhachet-roles-ehmpathy": "1.18.0",
93
+ "rhachet": "1.37.18",
94
+ "rhachet-brains-anthropic": "0.4.0",
95
+ "rhachet-roles-bhrain": "0.20.2",
96
+ "rhachet-roles-bhuild": "0.14.4",
97
+ "rhachet-roles-ehmpathy": "1.29.2",
98
98
  "test-fns": "1.10.0",
99
99
  "tsc-alias": "1.8.10",
100
100
  "tsx": "4.20.6",