rhachet-brains-openai 0.2.1 → 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.
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,55 @@ 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 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
+
805
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
+ };
806
853
  var genBrainAtom = (input) => {
807
854
  const config = CONFIG_BY_ATOM_SLUG[input.slug];
808
855
  return new import_brains.BrainAtom({
@@ -811,32 +858,52 @@ var genBrainAtom = (input) => {
811
858
  description: config.description,
812
859
  spec: config.spec,
813
860
  /**
814
- * .what = stateless inference (no tool use)
815
- * .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
816
863
  */
817
864
  ask: async (askInput, context) => {
818
865
  const startTime = Date.now();
819
866
  const systemPrompt = askInput.role.briefs ? await (0, import_brains.castBriefsToPrompt)({ briefs: askInput.role.briefs }) : void 0;
820
867
  const openai = context?.openai ?? new import_openai.default({ apiKey: process.env.OPENAI_API_KEY });
821
- const jsonSchema = import_zod.z.toJSONSchema(askInput.schema.output);
868
+ const jsonSchema = asJsonSchema({ schema: askInput.schema.output });
822
869
  const isObjectSchema = typeof jsonSchema === "object" && jsonSchema !== null && "type" in jsonSchema && jsonSchema.type === "object";
823
- 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}
824
874
 
825
875
  ---
826
876
 
827
877
  ${askInput.prompt}` : askInput.prompt;
878
+ const openaiFunctionTools = hasTools ? pluggedTools.map(
879
+ (definition) => castIntoOpenaiFunctionTool({ definition })
880
+ ) : [];
828
881
  const priorMessages = askInput.on?.episode?.exchanges.flatMap((exchange) => [
829
882
  { role: "user", content: exchange.input },
830
- { role: "assistant", content: exchange.output }
883
+ ...reconstructAssistantItems(exchange.output)
831
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
+ ] : [];
832
895
  const inputMessages = [
896
+ ...systemInstruction,
833
897
  ...priorMessages,
834
- { role: "user", content: fullPrompt }
898
+ ...currentInput
835
899
  ];
836
900
  const response = await openai.responses.create({
837
901
  model: config.model,
838
902
  input: inputMessages,
839
- ...isObjectSchema && {
903
+ ...hasTools && {
904
+ tools: openaiFunctionTools
905
+ },
906
+ ...!hasTools && isObjectSchema && {
840
907
  text: {
841
908
  format: {
842
909
  type: "json_schema",
@@ -847,6 +914,14 @@ ${askInput.prompt}` : askInput.prompt;
847
914
  }
848
915
  }
849
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;
850
925
  const outputItem = response.output.find(
851
926
  (item) => item.type === "message"
852
927
  );
@@ -856,7 +931,7 @@ ${askInput.prompt}` : askInput.prompt;
856
931
  const tokensOutput = response.usage?.output_tokens ?? 0;
857
932
  const tokensCacheGet = response.usage?.input_tokens_details?.cached_tokens ?? 0;
858
933
  const elapsedMs = Date.now() - startTime;
859
- const charsInput = fullPrompt.length;
934
+ const charsInput = isToolExecutionArray ? JSON.stringify(askInput.prompt).length : fullPrompt.length;
860
935
  const charsOutput = content.length;
861
936
  const size = {
862
937
  tokens: {
@@ -881,7 +956,38 @@ ${askInput.prompt}` : askInput.prompt;
881
956
  cash
882
957
  }
883
958
  });
884
- const output = isObjectSchema ? askInput.schema.output.parse(JSON.parse(content)) : askInput.schema.output.parse(content);
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;
885
991
  const continuables = await (0, import_brains.genBrainContinuables)({
886
992
  for: { grain: "atom" },
887
993
  on: {
@@ -890,14 +996,15 @@ ${askInput.prompt}` : askInput.prompt;
890
996
  },
891
997
  with: {
892
998
  exchange: {
893
- input: askInput.prompt,
894
- output: content,
999
+ input: exchangeInput,
1000
+ output: exchangeOutput,
895
1001
  exid: response.id
896
1002
  }
897
1003
  }
898
1004
  });
899
1005
  return new import_brains.BrainOutput({
900
1006
  output,
1007
+ calls,
901
1008
  metrics,
902
1009
  episode: continuables.episode,
903
1010
  series: continuables.series
@@ -1264,14 +1371,6 @@ var Codex = class {
1264
1371
  var import_helpful_errors = require("helpful-errors");
1265
1372
  var import_brains2 = require("rhachet/brains");
1266
1373
  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
1374
  var CONFIG_BY_REPL_SLUG = {
1276
1375
  // default
1277
1376
  "openai/codex": CONFIG_BY_ATOM_SLUG["openai/gpt/codex/5.1-max"],
@@ -1384,6 +1483,7 @@ var invokeCodex = async (input) => {
1384
1483
  });
1385
1484
  return new import_brains2.BrainOutput({
1386
1485
  output,
1486
+ calls: null,
1387
1487
  metrics,
1388
1488
  episode: continuables.episode,
1389
1489
  series: continuables.series
@@ -1396,11 +1496,10 @@ var genBrainRepl = (input) => {
1396
1496
  slug: input.slug,
1397
1497
  description: config.description,
1398
1498
  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({
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({
1404
1503
  mode: "ask",
1405
1504
  model: config.model,
1406
1505
  spec: config.spec,
@@ -1409,12 +1508,8 @@ var genBrainRepl = (input) => {
1409
1508
  role: askInput.role,
1410
1509
  prompt: askInput.prompt,
1411
1510
  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({
1511
+ })),
1512
+ act: (async (actInput, _context) => invokeCodex({
1418
1513
  mode: "act",
1419
1514
  model: config.model,
1420
1515
  spec: config.spec,
@@ -1423,7 +1518,7 @@ var genBrainRepl = (input) => {
1423
1518
  role: actInput.role,
1424
1519
  prompt: actInput.prompt,
1425
1520
  schema: actInput.schema
1426
- })
1521
+ }))
1427
1522
  });
1428
1523
  };
1429
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.1",
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 --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.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",
98
98
  "test-fns": "1.10.0",
99
99
  "tsc-alias": "1.8.10",
100
100
  "tsx": "4.20.6",