@codemation/core-nodes 0.4.3 → 1.0.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,69 +1,77 @@
1
1
  import type { AgentMessageDto, AgentToolCall } from "@codemation/core";
2
2
 
3
- import { AIMessage, HumanMessage, SystemMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
3
+ import type { AssistantModelMessage, ModelMessage, ToolModelMessage } from "ai";
4
4
 
5
+ import type { ExecutedToolCall } from "./aiAgentSupport.types";
6
+
7
+ /**
8
+ * AI-SDK-shaped message construction for the AIAgent stack. Emits plain `ModelMessage[]`
9
+ * ( `{ role: 'system' | 'user' | 'assistant' | 'tool', content: ... }` ) as consumed by
10
+ * `generateText({ messages })` from the `ai` package.
11
+ */
5
12
  export class AgentMessageFactory {
6
- static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<BaseMessage> {
13
+ static createPromptMessages(messages: ReadonlyArray<AgentMessageDto>): ReadonlyArray<ModelMessage> {
7
14
  return messages.map((message) => this.createPromptMessage(message));
8
15
  }
9
16
 
10
- static createSystemPrompt(systemMessage: string): SystemMessage {
11
- return new SystemMessage(systemMessage);
12
- }
13
-
14
- static createUserPrompt(prompt: string): HumanMessage {
15
- return new HumanMessage(prompt);
16
- }
17
-
18
- static createAssistantPrompt(prompt: string): AIMessage {
19
- return new AIMessage(prompt);
20
- }
21
-
22
- static createToolMessage(toolCallId: string, content: string): ToolMessage {
23
- return new ToolMessage({ tool_call_id: toolCallId, content });
24
- }
25
-
26
- static extractContent(message: unknown): string {
27
- if (typeof message === "string") return message;
28
- if (!this.isRecord(message)) return String(message);
29
- const content = message.content;
30
- if (typeof content === "string") return content;
31
- if (Array.isArray(content)) {
32
- return content
33
- .map((part) => {
34
- if (typeof part === "string") return part;
35
- if (this.isRecord(part) && typeof part.text === "string") return part.text;
36
- return JSON.stringify(part);
37
- })
38
- .join("\n");
17
+ /**
18
+ * Builds the assistant message that contains optional text plus one or more tool-call parts,
19
+ * matching the shape AI SDK emits between steps.
20
+ */
21
+ static createAssistantWithToolCalls(
22
+ text: string | undefined,
23
+ toolCalls: ReadonlyArray<AgentToolCall>,
24
+ ): AssistantModelMessage {
25
+ const content: AssistantModelMessage["content"] = [];
26
+ if (text && text.length > 0) {
27
+ content.push({ type: "text", text });
39
28
  }
40
- return JSON.stringify(content);
29
+ for (const toolCall of toolCalls) {
30
+ content.push({
31
+ type: "tool-call",
32
+ toolCallId: toolCall.id ?? toolCall.name,
33
+ toolName: toolCall.name,
34
+ input: toolCall.input ?? {},
35
+ });
36
+ }
37
+ return { role: "assistant", content };
41
38
  }
42
39
 
43
- static extractToolCalls(message: unknown): ReadonlyArray<AgentToolCall> {
44
- if (!this.isRecord(message)) return [];
45
- const toolCalls = message.tool_calls;
46
- if (!Array.isArray(toolCalls)) return [];
47
- return toolCalls
48
- .filter((toolCall) => this.isRecord(toolCall) && typeof toolCall.name === "string")
49
- .map((toolCall) => ({
50
- id: typeof toolCall.id === "string" ? toolCall.id : undefined,
51
- name: toolCall.name as string,
52
- input: this.isRecord(toolCall) && "args" in toolCall ? toolCall.args : undefined,
53
- }));
40
+ /**
41
+ * Builds the `{ role: "tool", content: [{ type: "tool-result", ... }, ...] }` message returned
42
+ * to the model after each tool round.
43
+ */
44
+ static createToolResultsMessage(executedToolCalls: ReadonlyArray<ExecutedToolCall>): ToolModelMessage {
45
+ return {
46
+ role: "tool",
47
+ content: executedToolCalls.map((executed) => ({
48
+ type: "tool-result",
49
+ toolCallId: executed.toolCallId,
50
+ toolName: executed.toolName,
51
+ output: {
52
+ type: "json",
53
+ value: AgentMessageFactory.toToolResultJson(executed.result),
54
+ },
55
+ })),
56
+ };
54
57
  }
55
58
 
56
- private static isRecord(value: unknown): value is Record<string, unknown> {
57
- return typeof value === "object" && value !== null;
59
+ private static toToolResultJson(value: unknown): import("ai").JSONValue {
60
+ if (value === undefined) return null;
61
+ try {
62
+ return JSON.parse(JSON.stringify(value)) as import("ai").JSONValue;
63
+ } catch {
64
+ return String(value);
65
+ }
58
66
  }
59
67
 
60
- private static createPromptMessage(message: AgentMessageDto): BaseMessage {
68
+ private static createPromptMessage(message: AgentMessageDto): ModelMessage {
61
69
  if (message.role === "system") {
62
- return this.createSystemPrompt(message.content);
70
+ return { role: "system", content: message.content };
63
71
  }
64
72
  if (message.role === "assistant") {
65
- return this.createAssistantPrompt(message.content);
73
+ return { role: "assistant", content: message.content };
66
74
  }
67
- return this.createUserPrompt(message.content);
75
+ return { role: "user", content: message.content };
68
76
  }
69
77
  }
@@ -1,18 +1,13 @@
1
- import type {
2
- ChatModelConfig,
3
- ChatModelStructuredOutputOptions,
4
- LangChainChatModelLike,
5
- LangChainStructuredOutputModelLike,
6
- ZodSchemaAny,
7
- } from "@codemation/core";
1
+ import type { ChatLanguageModel, ChatModelConfig, StructuredOutputOptions, ZodSchemaAny } from "@codemation/core";
8
2
  import { inject, injectable } from "@codemation/core";
9
3
 
10
- import { AIMessage, type BaseMessage } from "@langchain/core/messages";
4
+ import type { ModelMessage } from "ai";
11
5
  import { ZodError } from "zod";
12
6
 
13
- import { OpenAIStructuredOutputMethodFactory } from "../chatModels/OpenAIStructuredOutputMethodFactory";
14
- import { AgentMessageFactory } from "./AgentMessageFactory";
7
+ import { OpenAIChatModelFactory } from "../chatModels/OpenAIChatModelFactory";
8
+ import { OpenAiStrictJsonSchemaFactory } from "../chatModels/OpenAiStrictJsonSchemaFactory";
15
9
  import { AgentStructuredOutputRepairPromptFactory } from "./AgentStructuredOutputRepairPromptFactory";
10
+ import { AgentMessageFactory } from "./AgentMessageFactory";
16
11
 
17
12
  interface ParsedStructuredOutputSuccess<TValue> {
18
13
  readonly ok: true;
@@ -27,50 +22,54 @@ interface ParsedStructuredOutputFailure {
27
22
 
28
23
  type ParsedStructuredOutputResult<TValue> = ParsedStructuredOutputSuccess<TValue> | ParsedStructuredOutputFailure;
29
24
 
25
+ export type StructuredOutputSchemaForModel = ZodSchemaAny | Readonly<Record<string, unknown>>;
26
+
27
+ /**
28
+ * Orchestrates a 2-attempt repair loop on top of `generateText({ output: Output.object(...) })`.
29
+ *
30
+ * Strategy:
31
+ * 1. If the caller already has a raw final text (from a prior tool-calling turn), try parsing it
32
+ * directly against the schema — fast path for models that already emit strict JSON.
33
+ * 2. Otherwise, run a native structured-output call via {@link invokeStructuredModel}. For the
34
+ * OpenAI-strict path, a {@link OpenAiStrictJsonSchemaFactory}-built JSON Schema record is
35
+ * handed to AI SDK's `jsonSchema(...)` wrapper (preserves `additionalProperties: false` at
36
+ * every object depth).
37
+ * 3. If the structured call fails (AI_NoObjectGeneratedError / ZodError / schema reject), run a
38
+ * text-mode repair prompt with the validation error appended, up to 2 attempts.
39
+ */
30
40
  @injectable()
31
41
  export class AgentStructuredOutputRunner {
32
42
  private static readonly repairAttemptCount = 2;
43
+ private static readonly structuredOutputSchemaName = "agent_output";
33
44
 
34
45
  constructor(
35
46
  @inject(AgentStructuredOutputRepairPromptFactory)
36
47
  private readonly repairPromptFactory: AgentStructuredOutputRepairPromptFactory,
37
- @inject(OpenAIStructuredOutputMethodFactory)
38
- private readonly openAiStructuredOutputMethodFactory: OpenAIStructuredOutputMethodFactory,
48
+ @inject(OpenAiStrictJsonSchemaFactory)
49
+ private readonly openAiStrictJsonSchemaFactory: OpenAiStrictJsonSchemaFactory,
39
50
  ) {}
40
51
 
41
52
  async resolve<TOutput>(
42
53
  args: Readonly<{
43
- model: LangChainChatModelLike;
54
+ model: ChatLanguageModel;
44
55
  chatModelConfig: ChatModelConfig;
45
56
  schema: ZodSchemaAny;
46
- conversation: ReadonlyArray<BaseMessage>;
47
- rawFinalResponse?: AIMessage;
57
+ conversation: ReadonlyArray<ModelMessage>;
58
+ rawFinalText?: string;
48
59
  agentName: string;
49
60
  nodeId: string;
50
- invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
61
+ invokeTextModel: (messages: ReadonlyArray<ModelMessage>) => Promise<{ text: string }>;
51
62
  invokeStructuredModel: (
52
- model: LangChainStructuredOutputModelLike,
53
- messages: ReadonlyArray<BaseMessage>,
63
+ schema: StructuredOutputSchemaForModel,
64
+ messages: ReadonlyArray<ModelMessage>,
65
+ options: StructuredOutputOptions | undefined,
54
66
  ) => Promise<unknown>;
55
67
  }>,
56
68
  ): Promise<TOutput> {
57
69
  let lastFailure: ParsedStructuredOutputFailure | undefined;
58
70
 
59
- if (args.rawFinalResponse) {
60
- const directResult = this.tryParseAndValidate<TOutput>(
61
- AgentMessageFactory.extractContent(args.rawFinalResponse),
62
- args.schema,
63
- );
64
- if (directResult.ok) {
65
- return directResult.value;
66
- }
67
- lastFailure = directResult;
68
- } else if (!this.supportsNativeStructuredOutput(args.model)) {
69
- const rawResponse = await args.invokeTextModel(args.conversation);
70
- const directResult = this.tryParseAndValidate<TOutput>(
71
- AgentMessageFactory.extractContent(rawResponse),
72
- args.schema,
73
- );
71
+ if (args.rawFinalText !== undefined) {
72
+ const directResult = this.tryParseAndValidate<TOutput>(args.rawFinalText, args.schema);
74
73
  if (directResult.ok) {
75
74
  return directResult.value;
76
75
  }
@@ -78,19 +77,18 @@ export class AgentStructuredOutputRunner {
78
77
  }
79
78
 
80
79
  try {
81
- const nativeStructuredModel = this.createStructuredOutputModel(args.model, args.chatModelConfig, args.schema);
82
- if (nativeStructuredModel) {
83
- const nativeResult = this.tryValidateStructuredValue<TOutput>(
84
- await args.invokeStructuredModel(nativeStructuredModel, args.conversation),
85
- args.schema,
86
- );
87
- if (nativeResult.ok) {
88
- return nativeResult.value;
89
- }
90
- lastFailure = nativeResult;
80
+ const structuredOptions = this.resolveStructuredOutputOptions(args.chatModelConfig);
81
+ const schemaForModel = this.resolveOutputSchemaForModel(args.schema, structuredOptions);
82
+ const nativeResult = this.tryValidateStructuredValue<TOutput>(
83
+ await args.invokeStructuredModel(schemaForModel, args.conversation, structuredOptions),
84
+ args.schema,
85
+ );
86
+ if (nativeResult.ok) {
87
+ return nativeResult.value;
91
88
  }
89
+ lastFailure = nativeResult;
92
90
  } catch (error) {
93
- lastFailure = {
91
+ lastFailure = lastFailure ?? {
94
92
  ok: false,
95
93
  invalidContent: "",
96
94
  validationError: `Native structured output failed: ${this.summarizeError(error)}`,
@@ -112,16 +110,16 @@ export class AgentStructuredOutputRunner {
112
110
  private async retryWithRepairPrompt<TOutput>(
113
111
  args: Readonly<{
114
112
  schema: ZodSchemaAny;
115
- conversation: ReadonlyArray<BaseMessage>;
113
+ conversation: ReadonlyArray<ModelMessage>;
116
114
  lastFailure: ParsedStructuredOutputFailure;
117
115
  agentName: string;
118
116
  nodeId: string;
119
- invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
117
+ invokeTextModel: (messages: ReadonlyArray<ModelMessage>) => Promise<{ text: string }>;
120
118
  }>,
121
119
  ): Promise<TOutput> {
122
120
  let failure = args.lastFailure;
123
121
  for (let attempt = 1; attempt <= AgentStructuredOutputRunner.repairAttemptCount; attempt++) {
124
- const repairMessages = [
122
+ const repairMessages: ReadonlyArray<ModelMessage> = [
125
123
  ...args.conversation,
126
124
  ...AgentMessageFactory.createPromptMessages(
127
125
  this.repairPromptFactory.create({
@@ -132,10 +130,7 @@ export class AgentStructuredOutputRunner {
132
130
  ),
133
131
  ];
134
132
  const repairResponse = await args.invokeTextModel(repairMessages);
135
- const repairResult = this.tryParseAndValidate<TOutput>(
136
- AgentMessageFactory.extractContent(repairResponse),
137
- args.schema,
138
- );
133
+ const repairResult = this.tryParseAndValidate<TOutput>(repairResponse.text, args.schema);
139
134
  if (repairResult.ok) {
140
135
  return repairResult.value;
141
136
  }
@@ -146,29 +141,27 @@ export class AgentStructuredOutputRunner {
146
141
  );
147
142
  }
148
143
 
149
- private createStructuredOutputModel(
150
- model: LangChainChatModelLike,
151
- chatModelConfig: ChatModelConfig,
152
- schema: ZodSchemaAny,
153
- ): LangChainStructuredOutputModelLike | undefined {
154
- if (!this.supportsNativeStructuredOutput(model)) {
144
+ /**
145
+ * Chooses strict mode for OpenAI chat-model configs, off otherwise. Extendable in future for
146
+ * other providers that adopt the same "supply a JSON Schema record directly" contract.
147
+ */
148
+ private resolveStructuredOutputOptions(chatModelConfig: ChatModelConfig): StructuredOutputOptions | undefined {
149
+ if (chatModelConfig.type !== OpenAIChatModelFactory) {
155
150
  return undefined;
156
151
  }
157
- const options = this.getStructuredOutputOptions(chatModelConfig);
158
- return model.withStructuredOutput(schema, options);
152
+ return { strict: true, schemaName: AgentStructuredOutputRunner.structuredOutputSchemaName };
159
153
  }
160
154
 
161
- private getStructuredOutputOptions(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
162
- return this.openAiStructuredOutputMethodFactory.create(chatModelConfig) ?? { strict: true };
163
- }
164
-
165
- private supportsNativeStructuredOutput(model: LangChainChatModelLike): model is LangChainChatModelLike & {
166
- withStructuredOutput: (
167
- outputSchema: ZodSchemaAny,
168
- config?: ChatModelStructuredOutputOptions,
169
- ) => LangChainStructuredOutputModelLike;
170
- } {
171
- return typeof model.withStructuredOutput === "function";
155
+ private resolveOutputSchemaForModel(
156
+ schema: ZodSchemaAny,
157
+ options: StructuredOutputOptions | undefined,
158
+ ): StructuredOutputSchemaForModel {
159
+ if (!options?.strict) {
160
+ return schema;
161
+ }
162
+ return this.openAiStrictJsonSchemaFactory.createStructuredOutputRecord(schema, {
163
+ schemaName: options.schemaName ?? AgentStructuredOutputRunner.structuredOutputSchemaName,
164
+ });
172
165
  }
173
166
 
174
167
  private tryParseAndValidate<TOutput>(content: string, schema: ZodSchemaAny): ParsedStructuredOutputResult<TOutput> {
@@ -214,7 +207,8 @@ export class AgentStructuredOutputRunner {
214
207
 
215
208
  private toJson(value: unknown): string {
216
209
  try {
217
- return JSON.stringify(value);
210
+ const serialized = JSON.stringify(value);
211
+ return serialized ?? String(value);
218
212
  } catch (error) {
219
213
  return `<<unserializable: ${this.summarizeError(error)}>>`;
220
214
  }
@@ -71,8 +71,8 @@ export class AgentToolExecutionCoordinator {
71
71
  });
72
72
 
73
73
  try {
74
- const serialized = await plannedToolCall.binding.langChainTool.invoke(plannedToolCall.toolCall.input ?? {});
75
- const result = this.parseToolOutput(serialized);
74
+ const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {});
75
+ const serialized = typeof result === "string" ? result : JSON.stringify(result);
76
76
  const finishedAt = new Date();
77
77
  await ctx.nodeState?.markCompleted({
78
78
  nodeId: plannedToolCall.nodeId,
@@ -113,7 +113,7 @@ export class AgentToolExecutionCoordinator {
113
113
  const classification = this.errorClassifier.classify({
114
114
  error,
115
115
  toolName: plannedToolCall.binding.config.name,
116
- schema: plannedToolCall.binding.langChainTool.schema,
116
+ schema: plannedToolCall.binding.inputSchema,
117
117
  });
118
118
 
119
119
  if (classification.kind !== "repairable_validation_error") {
@@ -324,17 +324,6 @@ export class AgentToolExecutionCoordinator {
324
324
  return `Your previous tool call for "${toolName}" was invalid because field "${fieldPath}" failed validation: ${firstIssue.message}`;
325
325
  }
326
326
 
327
- private parseToolOutput(serialized: unknown): unknown {
328
- if (typeof serialized !== "string") {
329
- return serialized;
330
- }
331
- try {
332
- return JSON.parse(serialized) as unknown;
333
- } catch {
334
- return serialized;
335
- }
336
- }
337
-
338
327
  private toJsonValue(value: unknown): JsonValue | undefined {
339
328
  if (value === undefined) {
340
329
  return undefined;
@@ -6,7 +6,6 @@ import type {
6
6
  ToolExecuteArgs,
7
7
  ZodSchemaAny,
8
8
  } from "@codemation/core";
9
- import type { DynamicStructuredTool } from "@langchain/core/tools";
10
9
 
11
10
  export class AgentItemPortMap {
12
11
  static fromItem(item: Item): NodeInputsByPort {
@@ -23,9 +22,15 @@ export type ResolvedTool = Readonly<{
23
22
  }>;
24
23
  }>;
25
24
 
25
+ /**
26
+ * Per-item binding of a tool: the user config plus the resolved runtime and a snapshot of the
27
+ * original Zod `inputSchema` used to convert to AI SDK `Tool` + OpenAI-strict JSON Schema for
28
+ * repair prompts.
29
+ */
26
30
  export type ItemScopedToolBinding = Readonly<{
27
31
  config: ToolConfig;
28
- langChainTool: DynamicStructuredTool;
32
+ inputSchema: ZodSchemaAny;
33
+ execute(input: unknown): Promise<unknown>;
29
34
  }>;
30
35
 
31
36
  export type PlannedToolCall = Readonly<{
@@ -1,46 +0,0 @@
1
- import type { ChatModelConfig, ChatModelStructuredOutputOptions } from "@codemation/core";
2
- import { injectable } from "@codemation/core";
3
-
4
- import { OpenAIChatModelFactory } from "./OpenAIChatModelFactory";
5
-
6
- @injectable()
7
- export class OpenAIStructuredOutputMethodFactory {
8
- private static readonly isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
9
-
10
- create(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
11
- if (chatModelConfig.type !== OpenAIChatModelFactory) {
12
- return undefined;
13
- }
14
- const model = this.readModelName(chatModelConfig);
15
- if (!model) {
16
- return { method: "functionCalling", strict: true };
17
- }
18
- return {
19
- method: this.supportsJsonSchema(model) ? "jsonSchema" : "functionCalling",
20
- strict: true,
21
- };
22
- }
23
-
24
- private readModelName(chatModelConfig: ChatModelConfig): string | undefined {
25
- const candidate = chatModelConfig as Readonly<{ model?: unknown }>;
26
- return typeof candidate.model === "string" ? candidate.model : undefined;
27
- }
28
-
29
- private supportsJsonSchema(model: string): boolean {
30
- if (model === "gpt-4o" || model === "gpt-4o-mini") {
31
- return true;
32
- }
33
- return (
34
- this.supportsSnapshotAtOrAfter(model, "gpt-4o-", "2024-08-06") ||
35
- this.supportsSnapshotAtOrAfter(model, "gpt-4o-mini-", "2024-07-18")
36
- );
37
- }
38
-
39
- private supportsSnapshotAtOrAfter(model: string, prefix: string, minimumSnapshotDate: string): boolean {
40
- if (!model.startsWith(prefix)) {
41
- return false;
42
- }
43
- const snapshotDate = model.slice(prefix.length);
44
- return OpenAIStructuredOutputMethodFactory.isoDatePattern.test(snapshotDate) && snapshotDate >= minimumSnapshotDate;
45
- }
46
- }