substrate-ai 0.17.0 → 0.17.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.
@@ -4046,7 +4046,7 @@ minimatch.unescape = unescape;
4046
4046
 
4047
4047
  //#endregion
4048
4048
  //#region src/modules/repo-map/query.ts
4049
- const DEFAULT_MAX_TOKENS = 2e3;
4049
+ const DEFAULT_MAX_TOKENS$1 = 2e3;
4050
4050
  /** Chars per token heuristic (same as ContextInjector) */
4051
4051
  const CHARS_PER_TOKEN = 4;
4052
4052
  /** Overhead chars per symbol line: `:lineNumber symbolType \n` */
@@ -4062,7 +4062,7 @@ var RepoMapQueryEngine = class {
4062
4062
  }
4063
4063
  async query(q) {
4064
4064
  const start = Date.now();
4065
- const maxTokens = q.maxTokens ?? DEFAULT_MAX_TOKENS;
4065
+ const maxTokens = q.maxTokens ?? DEFAULT_MAX_TOKENS$1;
4066
4066
  const hasFilesFilter = (q.files?.length ?? 0) > 0;
4067
4067
  const hasSymbolsFilter = (q.symbols?.length ?? 0) > 0;
4068
4068
  const hasTypesFilter = (q.types?.length ?? 0) > 0;
@@ -4912,9 +4912,9 @@ const coerceToString = z.preprocess((val) => {
4912
4912
  */
4913
4913
  const DevStoryResultSchema = z.object({
4914
4914
  result: z.preprocess((val) => val === "failure" ? "failed" : val, z.enum(["success", "failed"])),
4915
- ac_met: z.array(coerceToString),
4916
- ac_failures: z.array(coerceToString),
4917
- files_modified: z.array(z.string()),
4915
+ ac_met: z.array(coerceToString).default([]),
4916
+ ac_failures: z.array(coerceToString).default([]),
4917
+ files_modified: z.array(z.string()).default([]),
4918
4918
  tests: z.preprocess((val) => {
4919
4919
  if (typeof val === "string") {
4920
4920
  const lower = val.toLowerCase();
@@ -4936,7 +4936,11 @@ const DevStoryResultSchema = z.object({
4936
4936
  * as strings (`"42"` instead of `42`). This handles the conversion.
4937
4937
  */
4938
4938
  const coerceOptionalNumber = z.preprocess((val) => typeof val === "string" ? Number(val) : val, z.number().optional());
4939
- const coerceNumber = z.preprocess((val) => typeof val === "string" ? Number(val) : val, z.number());
4939
+ const coerceNumber = z.preprocess((val) => {
4940
+ if (typeof val === "string") return Number(val);
4941
+ if (Array.isArray(val)) return val.length;
4942
+ return val;
4943
+ }, z.number());
4940
4944
  /**
4941
4945
  * Schema for a single issue in the code review output.
4942
4946
  */
@@ -4956,13 +4960,13 @@ const CodeReviewIssueSchema = z.object({
4956
4960
  * status determination backed by concrete evidence from the diff.
4957
4961
  */
4958
4962
  const AcChecklistEntrySchema = z.object({
4959
- ac_id: z.string(),
4963
+ ac_id: z.string().default(""),
4960
4964
  status: z.enum([
4961
4965
  "met",
4962
4966
  "not_met",
4963
4967
  "partial"
4964
- ]),
4965
- evidence: z.string()
4968
+ ]).default("met"),
4969
+ evidence: z.string().default("")
4966
4970
  });
4967
4971
  /**
4968
4972
  * Compute the verdict from the issue list using deterministic rules.
@@ -28737,228 +28741,2440 @@ const ajv = new import_ajv.default({
28737
28741
  allErrors: true,
28738
28742
  coerceTypes: false
28739
28743
  });
28740
-
28741
- //#endregion
28742
- //#region packages/factory/dist/handlers/start.js
28743
28744
  /**
28744
- * Handler for start nodes.
28745
- * No side effects — edge selection drives the next step.
28746
- *
28747
- * Story 42-9.
28745
+ * Registry for tool definitions. Provides lookup, validation, and execution.
28748
28746
  */
28749
- /** Handler for start nodes; no side effects; edge selection drives next step. */
28750
- const startHandler = async (_node, _context, _graph) => {
28751
- return { status: "SUCCESS" };
28747
+ var ToolRegistry = class {
28748
+ _tools = new Map();
28749
+ register(tool) {
28750
+ this._tools.set(tool.name, tool);
28751
+ }
28752
+ async execute(name, args, env) {
28753
+ const tool = this._tools.get(name);
28754
+ if (!tool) return {
28755
+ content: `Unknown tool: ${name}`,
28756
+ isError: true
28757
+ };
28758
+ const validate$1 = ajv.compile(tool.inputSchema);
28759
+ const valid = validate$1(args);
28760
+ if (!valid) {
28761
+ const errors = validate$1.errors ?? [];
28762
+ const messages = errors.map((e) => {
28763
+ const path$2 = e.instancePath ?? e.dataPath ?? "";
28764
+ return `${path$2} ${e.message ?? ""}`.trim();
28765
+ }).join(", ");
28766
+ return {
28767
+ content: `Validation failed for tool '${name}': ${messages}`,
28768
+ isError: true
28769
+ };
28770
+ }
28771
+ try {
28772
+ const result = await tool.executor(args, env);
28773
+ const content = tool.outputTruncation !== void 0 && result.length > tool.outputTruncation ? result.slice(0, tool.outputTruncation) + "\n[truncated]" : result;
28774
+ return {
28775
+ content,
28776
+ isError: false
28777
+ };
28778
+ } catch (err) {
28779
+ const message = err instanceof Error ? err.message : String(err);
28780
+ return {
28781
+ content: message,
28782
+ isError: true
28783
+ };
28784
+ }
28785
+ }
28786
+ get(name) {
28787
+ return this._tools.get(name);
28788
+ }
28789
+ getDefinitions() {
28790
+ return Array.from(this._tools.values());
28791
+ }
28752
28792
  };
28753
28793
 
28754
28794
  //#endregion
28755
- //#region packages/factory/dist/handlers/exit.js
28756
- /**
28757
- * Handler for exit/terminal nodes.
28758
- * Signals successful graph completion with no side effects.
28759
- *
28760
- * Story 42-9.
28761
- */
28762
- /** Handler for exit/terminal nodes; signals successful graph completion. */
28763
- const exitHandler = async (_node, _context, _graph) => {
28764
- return { status: "SUCCESS" };
28795
+ //#region packages/factory/dist/agent/types.js
28796
+ const DEFAULT_SESSION_CONFIG = {
28797
+ max_turns: 0,
28798
+ max_tool_rounds_per_input: 0,
28799
+ default_command_timeout_ms: 1e4,
28800
+ max_command_timeout_ms: 6e5,
28801
+ reasoning_effort: null,
28802
+ tool_output_limits: new Map(),
28803
+ enable_loop_detection: true,
28804
+ loop_detection_window: 10,
28805
+ truncation_mode: "head_tail",
28806
+ max_output_lines: 500
28807
+ };
28808
+ const SessionState = {
28809
+ IDLE: "IDLE",
28810
+ PROCESSING: "PROCESSING",
28811
+ AWAITING_INPUT: "AWAITING_INPUT",
28812
+ CLOSED: "CLOSED"
28813
+ };
28814
+ const EventKind = {
28815
+ SESSION_START: "SESSION_START",
28816
+ SESSION_END: "SESSION_END",
28817
+ USER_INPUT: "USER_INPUT",
28818
+ PROCESSING_END: "PROCESSING_END",
28819
+ ASSISTANT_TEXT_START: "ASSISTANT_TEXT_START",
28820
+ ASSISTANT_TEXT_DELTA: "ASSISTANT_TEXT_DELTA",
28821
+ ASSISTANT_TEXT_END: "ASSISTANT_TEXT_END",
28822
+ TOOL_CALL_START: "TOOL_CALL_START",
28823
+ TOOL_CALL_OUTPUT_DELTA: "TOOL_CALL_OUTPUT_DELTA",
28824
+ TOOL_CALL_END: "TOOL_CALL_END",
28825
+ STEERING_INJECTED: "STEERING_INJECTED",
28826
+ TURN_LIMIT: "TURN_LIMIT",
28827
+ LOOP_DETECTION: "LOOP_DETECTION",
28828
+ WARNING: "WARNING",
28829
+ ERROR: "ERROR"
28765
28830
  };
28766
28831
 
28767
28832
  //#endregion
28768
- //#region packages/factory/dist/handlers/conditional.js
28833
+ //#region packages/factory/dist/agent/truncation.js
28769
28834
  /**
28770
- * Handler for conditional/branching nodes.
28771
- * Routing is delegated entirely to edge selection (story 42-12).
28772
- * This handler does nothing — it simply returns SUCCESS so the executor
28773
- * can proceed to edge evaluation.
28835
+ * Default per-tool character output limits.
28836
+ * Tools not listed here fall back to DEFAULT_FALLBACK_CHAR_LIMIT (10,000).
28774
28837
  *
28775
- * Story 42-9.
28776
- */
28777
- /**
28778
- * Handler for conditional/branching nodes; routing is delegated entirely to
28779
- * edge selection (story 42-12); this handler does nothing.
28838
+ * Note: shell is 30K (not 10K as in the AC) — shell output often contains build logs,
28839
+ * test results, and compiler errors that need more context for effective debugging.
28840
+ * The 10K AC value was based on interactive shell use; 30K reflects agentic workloads.
28780
28841
  */
28781
- const conditionalHandler = async (_node, _context, _graph) => {
28782
- return { status: "SUCCESS" };
28842
+ const DEFAULT_TOOL_LIMITS = {
28843
+ read_file: 5e4,
28844
+ shell: 3e4,
28845
+ grep: 2e4,
28846
+ glob: 2e4
28783
28847
  };
28848
+ /** Fallback character limit for tools not in DEFAULT_TOOL_LIMITS */
28849
+ const DEFAULT_FALLBACK_CHAR_LIMIT = 1e4;
28850
+ /** Maximum lines before Phase 2 line-based truncation is applied */
28851
+ const DEFAULT_LINE_LIMIT = 500;
28852
+ function truncateByChars(output, limit, mode) {
28853
+ if (output.length <= limit) return output;
28854
+ if (mode === "tail") return output.slice(-limit);
28855
+ const half = Math.floor(limit / 2);
28856
+ const removed = output.length - limit;
28857
+ return output.slice(0, half) + `\n\n[... ${removed} characters truncated from middle. Full output available in event stream.]\n\n` + output.slice(-half);
28858
+ }
28859
+ function truncateByLines(output, maxLines, mode) {
28860
+ const lines = output.split("\n");
28861
+ if (lines.length <= maxLines) return output;
28862
+ if (mode === "tail") return lines.slice(-maxLines).join("\n");
28863
+ const headCount = Math.ceil(maxLines / 2);
28864
+ const tailCount = Math.floor(maxLines / 2);
28865
+ const removed = lines.length - maxLines;
28866
+ return lines.slice(0, headCount).join("\n") + `\n[... ${removed} lines truncated from middle ...]\n` + lines.slice(-tailCount).join("\n");
28867
+ }
28868
+ /**
28869
+ * Truncates tool output to fit within LLM context windows using a two-phase pipeline.
28870
+ *
28871
+ * Phase 1 (character-based): Applies per-tool character limits in head_tail or tail mode.
28872
+ * Phase 2 (line-based): Applies max_output_lines limit in head_tail or tail mode.
28873
+ *
28874
+ * If output is within both limits, returns the original string unchanged.
28875
+ */
28876
+ function truncateToolOutput(output, toolName, config) {
28877
+ const charLimit = config.tool_output_limits.get(toolName) ?? DEFAULT_TOOL_LIMITS[toolName] ?? DEFAULT_FALLBACK_CHAR_LIMIT;
28878
+ const mode = config.truncation_mode ?? "head_tail";
28879
+ const maxLines = config.max_output_lines ?? DEFAULT_LINE_LIMIT;
28880
+ const afterPhase1 = truncateByChars(output, charLimit, mode);
28881
+ return truncateByLines(afterPhase1, maxLines, mode);
28882
+ }
28784
28883
 
28785
28884
  //#endregion
28786
- //#region packages/factory/dist/stylesheet/resolver.js
28787
- /**
28788
- * Specificity-based resolver for the model stylesheet.
28789
- *
28790
- * Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
28791
- * which LLM routing properties apply to that node by:
28792
- * 1. Filtering rules to only those whose selector matches the node.
28793
- * 2. Sorting matching rules by specificity ascending (stable sort so that
28794
- * equal-specificity rules preserve source order).
28795
- * 3. Iterating the sorted list and letting each rule overwrite properties
28796
- * the last rule at the highest specificity wins; for ties the rule
28797
- * appearing later in the original stylesheet wins.
28798
- *
28799
- * **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
28800
- * attribute wins" rule. The caller (executor / node preparation layer) is
28801
- * responsible for the final merge:
28802
- * ```typescript
28803
- * const resolved = resolveNodeStyles(node, stylesheet)
28804
- * const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
28805
- * ```
28806
- *
28807
- * Story 42-7.
28808
- */
28809
- /**
28810
- * Return `true` if the given `selector` matches the provided `node`.
28811
- *
28812
- * Matching rules:
28813
- * - `universal`: always matches every node.
28814
- * - `shape`: matches when `node.shape === selector.value`.
28815
- * - `class`: matches when the node's `class` field, split on commas and
28816
- * trimmed, contains `selector.value` (case-sensitive).
28817
- * - `id`: matches when `node.id === selector.value`.
28818
- */
28819
- function matchesNode(node, selector) {
28820
- switch (selector.type) {
28821
- case "universal": return true;
28822
- case "shape": return node.shape === selector.value;
28823
- case "class": {
28824
- const tokens = node.class.split(",").map((t) => t.trim());
28825
- return tokens.includes(selector.value);
28885
+ //#region packages/factory/dist/agent/loop-detection.js
28886
+ var LoopDetector = class {
28887
+ _config;
28888
+ _window = [];
28889
+ constructor(config) {
28890
+ this._config = config;
28891
+ }
28892
+ /**
28893
+ * Record a tool call and check for a repeating pattern.
28894
+ * Returns true if a loop is detected (window is full and a repeating pattern found).
28895
+ * Returns false if detection is disabled, window is not yet full, or no pattern is found.
28896
+ */
28897
+ record(toolName, toolArgs) {
28898
+ if (!this._config.enabled) return false;
28899
+ const sig = createHash("sha256").update(`${toolName}:${JSON.stringify(toolArgs)}`).digest("hex");
28900
+ this._window.push(sig);
28901
+ if (this._window.length > this._config.windowSize) this._window.shift();
28902
+ if (this._window.length < this._config.windowSize) return false;
28903
+ return this._detectPattern();
28904
+ }
28905
+ /**
28906
+ * Check whether the current window contains a repeating pattern of length 1, 2, or 3.
28907
+ * Pattern length N is only checked if windowSize % N === 0.
28908
+ */
28909
+ _detectPattern() {
28910
+ const w = this._window;
28911
+ const n$1 = this._config.windowSize;
28912
+ for (const patternLen of [
28913
+ 1,
28914
+ 2,
28915
+ 3
28916
+ ]) {
28917
+ if (n$1 % patternLen !== 0) continue;
28918
+ const pattern = w.slice(0, patternLen);
28919
+ let allMatch = true;
28920
+ for (let i = patternLen; i < n$1; i += patternLen) {
28921
+ for (let j$1 = 0; j$1 < patternLen; j$1++) if (w[i + j$1] !== pattern[j$1]) {
28922
+ allMatch = false;
28923
+ break;
28924
+ }
28925
+ if (!allMatch) break;
28926
+ }
28927
+ if (allMatch) return true;
28826
28928
  }
28827
- case "id": return node.id === selector.value;
28828
- default: return false;
28929
+ return false;
28829
28930
  }
28830
- }
28831
- /**
28832
- * Resolve LLM routing properties for a single graph node using the parsed stylesheet.
28833
- *
28834
- * @param node - The graph node to resolve properties for.
28835
- * @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
28836
- * @returns A `ResolvedNodeStyles` object containing only the properties that
28837
- * were resolved from matching rules. Properties absent from matching rules
28838
- * are omitted (not set to `undefined` explicitly).
28839
- *
28840
- * **Important**: this function does NOT check the node's own attributes.
28841
- * Callers must give explicit node attributes (`node.llmModel`, etc.) priority
28842
- * over the values returned here.
28843
- */
28844
- function resolveNodeStyles(node, stylesheet) {
28845
- const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
28846
- matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
28847
- const resolved = {};
28848
- for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
28849
- else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
28850
- else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
28851
- return resolved;
28852
- }
28931
+ };
28853
28932
 
28854
28933
  //#endregion
28855
- //#region packages/factory/dist/handlers/codergen-handler.js
28856
- const DEFAULT_MODEL = "claude-sonnet-4-5";
28857
- const DEFAULT_PROVIDER = "anthropic";
28858
- const DEFAULT_REASONING_EFFORT = "medium";
28934
+ //#region packages/factory/dist/agent/loop.js
28935
+ /** Build a ToolRegistry from a ProviderProfile's tool definitions. */
28936
+ function buildRegistryFromProfile(profile) {
28937
+ const registry = new ToolRegistry();
28938
+ for (const tool of profile.tools()) registry.register(tool);
28939
+ return registry;
28940
+ }
28859
28941
  /**
28860
- * Replace all `{{key}}` placeholders in `template` with the corresponding
28861
- * values from `context`. Missing keys resolve to empty string `""` without
28862
- * throwing.
28863
- *
28864
- * @param template - The prompt template string (may contain `{{variable}}` tokens).
28865
- * @param context - The graph context to read values from.
28866
- * @returns The fully-interpolated prompt string.
28867
- */
28868
- function interpolatePrompt(template, context) {
28869
- return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
28870
- return context.getString(key, "");
28871
- });
28942
+ * Convert session history turns to LLMMessage array for the LLM request.
28943
+ * SystemTurns are skipped (system prompt is provided separately via ProviderProfile).
28944
+ */
28945
+ function convertHistoryToMessages(history) {
28946
+ const messages = [];
28947
+ for (const turn of history) switch (turn.type) {
28948
+ case "user":
28949
+ case "steering":
28950
+ messages.push({
28951
+ role: "user",
28952
+ content: [{
28953
+ kind: "text",
28954
+ text: turn.content
28955
+ }]
28956
+ });
28957
+ break;
28958
+ case "assistant":
28959
+ if (turn.tool_calls.length > 0) {
28960
+ const parts = [];
28961
+ if (turn.content) parts.push({
28962
+ kind: "text",
28963
+ text: turn.content
28964
+ });
28965
+ for (const tc of turn.tool_calls) parts.push({
28966
+ kind: "tool_call",
28967
+ toolCall: {
28968
+ id: tc.id,
28969
+ name: tc.name,
28970
+ arguments: tc.arguments,
28971
+ ...tc.rawArguments !== void 0 ? { rawArguments: tc.rawArguments } : {}
28972
+ }
28973
+ });
28974
+ messages.push({
28975
+ role: "assistant",
28976
+ content: parts
28977
+ });
28978
+ } else messages.push({
28979
+ role: "assistant",
28980
+ content: [{
28981
+ kind: "text",
28982
+ text: turn.content
28983
+ }]
28984
+ });
28985
+ break;
28986
+ case "tool_results":
28987
+ messages.push({
28988
+ role: "user",
28989
+ content: turn.results.map((r) => ({
28990
+ kind: "tool_result",
28991
+ toolResult: {
28992
+ toolCallId: r.tool_call_id,
28993
+ content: r.content,
28994
+ isError: r.is_error
28995
+ }
28996
+ }))
28997
+ });
28998
+ break;
28999
+ case "system": break;
29000
+ }
29001
+ return messages;
28872
29002
  }
28873
29003
  /**
28874
- * Resolve the LLM routing properties for a graph node.
28875
- *
28876
- * Priority (highest wins):
28877
- * 1. Node-level attributes (`node.llmModel`, `node.llmProvider`, `node.reasoningEffort`)
28878
- * 2. Stylesheet-resolved values via `resolveNodeStyles`
28879
- * 3. Per-option defaults (`options.defaultModel`, etc.)
28880
- * 4. System defaults (`claude-sonnet-4-5`, `anthropic`, `medium`)
29004
+ * Build LLMRequest from the current session state.
28881
29005
  */
28882
- function resolveModel(node, stylesheet, options) {
28883
- const stylesheetResolved = stylesheet ? resolveNodeStyles(node, stylesheet) : {};
28884
- const llm_model = node.llmModel || stylesheetResolved.llmModel || options?.defaultModel || DEFAULT_MODEL;
28885
- const llm_provider = node.llmProvider || stylesheetResolved.llmProvider || options?.defaultProvider || DEFAULT_PROVIDER;
28886
- const reasoning_effort = node.reasoningEffort || stylesheetResolved.reasoningEffort || options?.defaultReasoningEffort || DEFAULT_REASONING_EFFORT;
28887
- return {
28888
- llm_model,
28889
- llm_provider,
28890
- reasoning_effort
29006
+ function buildLLMRequest(session) {
29007
+ const profile = session.providerProfile;
29008
+ const config = session.config;
29009
+ const tools = profile.tools().map((t) => ({
29010
+ name: t.name,
29011
+ description: t.description,
29012
+ parameters: t.inputSchema
29013
+ }));
29014
+ const providerOptions = profile.provider_options();
29015
+ const { max_tokens, temperature,...rest } = providerOptions;
29016
+ const req = {
29017
+ model: profile.model,
29018
+ systemPrompt: profile.build_system_prompt(),
29019
+ messages: convertHistoryToMessages(session.history),
29020
+ tools,
29021
+ toolChoice: "auto"
28891
29022
  };
29023
+ if (max_tokens !== void 0) req.maxTokens = max_tokens;
29024
+ if (temperature !== void 0) req.temperature = temperature;
29025
+ if (config.reasoning_effort) req.reasoningEffort = config.reasoning_effort;
29026
+ if (Object.keys(rest).length > 0) req.extra = rest;
29027
+ return req;
28892
29028
  }
28893
- /**
28894
- * Returns `true` for errors that are transient and should trigger a
28895
- * `NEEDS_RETRY` outcome:
28896
- * - HTTP status 429 (rate limit)
28897
- * - Errors whose message contains `"timeout"`, `"ETIMEDOUT"`,
28898
- * `"ECONNRESET"`, or `"ECONNREFUSED"`
28899
- *
28900
- * All other errors are non-transient and map to `FAILURE`.
28901
- */
28902
- function isTransientError(error) {
28903
- if (error == null) return false;
28904
- const status = error.status ?? error.statusCode;
28905
- if (status === 429) return true;
28906
- const message = error.message;
28907
- if (typeof message === "string") {
28908
- if (message.includes("timeout") || message.includes("ETIMEDOUT") || message.includes("ECONNRESET") || message.includes("ECONNREFUSED")) return true;
29029
+ var CodingAgentSession = class {
29030
+ id;
29031
+ state;
29032
+ history;
29033
+ config;
29034
+ llmClient;
29035
+ providerProfile;
29036
+ executionEnv;
29037
+ _steeringQueue;
29038
+ _followupQueue;
29039
+ _emitter;
29040
+ _abortController;
29041
+ _toolRegistry;
29042
+ constructor(options) {
29043
+ this.id = randomUUID();
29044
+ this.state = SessionState.IDLE;
29045
+ this.history = [];
29046
+ this._steeringQueue = [];
29047
+ this._followupQueue = [];
29048
+ this._emitter = new EventEmitter();
29049
+ this._emitter.setMaxListeners(50);
29050
+ this._abortController = new AbortController();
29051
+ this.llmClient = options.llmClient;
29052
+ this.providerProfile = options.providerProfile;
29053
+ this.executionEnv = options.executionEnv;
29054
+ this.config = {
29055
+ ...DEFAULT_SESSION_CONFIG,
29056
+ ...options.config,
29057
+ tool_output_limits: options.config?.tool_output_limits ?? new Map(),
29058
+ truncation_mode: options.config?.truncation_mode ?? "head_tail",
29059
+ max_output_lines: options.config?.max_output_lines ?? DEFAULT_LINE_LIMIT
29060
+ };
29061
+ this._toolRegistry = buildRegistryFromProfile(options.providerProfile);
29062
+ this._emit(EventKind.SESSION_START, { session_id: this.id });
28909
29063
  }
28910
- return false;
28911
- }
28912
- /**
28913
- * Create a codergen node handler configured with the given options.
28914
- *
28915
- * @param options - Optional configuration (stylesheet, default model/provider/effort).
28916
- * @returns A `NodeHandler` that:
28917
- * 1. Interpolates the node's prompt (or label) against GraphContext.
28918
- * 2. Resolves LLM routing properties.
28919
- * 3. Calls the LLM client from `\@substrate-ai/core`.
28920
- * 4. Returns a `SUCCESS` outcome with the response in `contextUpdates`,
28921
- * `NEEDS_RETRY` for transient errors, or `FAILURE` for others.
28922
- */
28923
- function createCodergenHandler(options) {
28924
- return async (node, context, _graph) => {
28925
- const template = node.prompt || node.label || "";
28926
- const interpolatedPrompt = interpolatePrompt(template, context);
28927
- if (options?.backend) return options.backend.run(node, interpolatedPrompt, context);
28928
- if (node.backend === "direct" && options?.directBackend) return options.directBackend.run(node, interpolatedPrompt, context);
28929
- const { llm_model, llm_provider, reasoning_effort } = resolveModel(node, options?.stylesheet, options);
28930
- try {
28931
- const result = await callLLM({
28932
- model: llm_model,
28933
- provider: llm_provider,
28934
- reasoningEffort: reasoning_effort,
28935
- prompt: interpolatedPrompt
28936
- });
28937
- const responseText = result.text;
28938
- return {
28939
- status: "SUCCESS",
28940
- notes: responseText,
28941
- contextUpdates: { [`${node.id}_output`]: responseText }
28942
- };
28943
- } catch (error) {
28944
- if (isTransientError(error)) return {
28945
- status: "NEEDS_RETRY",
28946
- error
28947
- };
28948
- return {
28949
- status: "FAILURE",
28950
- error
29064
+ /** Subscribe to session events. */
29065
+ on(kind, handler) {
29066
+ this._emitter.on(kind, handler);
29067
+ }
29068
+ /** Transition to CLOSED state and emit SESSION_END. */
29069
+ close() {
29070
+ this.state = SessionState.CLOSED;
29071
+ this._emit(EventKind.SESSION_END, {});
29072
+ }
29073
+ /** Abort the current operation and close the session. */
29074
+ abort() {
29075
+ this._abortController.abort();
29076
+ this.close();
29077
+ }
29078
+ /**
29079
+ * Push a steering message onto the steering queue.
29080
+ * It will be injected into history (as a SteeringTurn) before the next LLM call.
29081
+ * Safe to call from any state (IDLE, PROCESSING, CLOSED); if CLOSED, the message
29082
+ * is queued but will never be drained.
29083
+ */
29084
+ steer(message) {
29085
+ this._steeringQueue.push(message);
29086
+ }
29087
+ /**
29088
+ * Push a follow-up message onto the follow-up queue.
29089
+ * After the current processInput cycle completes naturally (no more tool calls),
29090
+ * the first queued follow-up triggers a new processInput cycle recursively.
29091
+ * PROCESSING_END is not emitted until all follow-ups are exhausted.
29092
+ * Safe to call from any state.
29093
+ */
29094
+ follow_up(message) {
29095
+ this._followupQueue.push(message);
29096
+ }
29097
+ /**
29098
+ * Construct and emit a SessionEvent.
29099
+ * Internal — underscore prefix signals non-public API.
29100
+ */
29101
+ _emit(kind, data = {}) {
29102
+ const event = {
29103
+ kind,
29104
+ timestamp: new Date(),
29105
+ session_id: this.id,
29106
+ data
29107
+ };
29108
+ this._emitter.emit(kind, event);
29109
+ }
29110
+ /**
29111
+ * Dequeue all steering messages, append as SteeringTurns, emit STEERING_INJECTED.
29112
+ * Called before each LLM call and after tool execution.
29113
+ * Story 48-8 will extend this method.
29114
+ */
29115
+ _drainSteering() {
29116
+ while (this._steeringQueue.length > 0) {
29117
+ const content = this._steeringQueue.shift();
29118
+ const turn = {
29119
+ type: "steering",
29120
+ content,
29121
+ timestamp: new Date()
28951
29122
  };
29123
+ this.history.push(turn);
29124
+ this._emit(EventKind.STEERING_INJECTED, { content });
28952
29125
  }
28953
- };
28954
- }
28955
-
28956
- //#endregion
28957
- //#region packages/factory/dist/handlers/tool.js
28958
- /**
28959
- * Duck-typing guard: returns true if `parsed` conforms to the ScenarioRunResult
28960
- * shape (has summary.total and summary.passed as numbers).
28961
- *
29126
+ }
29127
+ /**
29128
+ * Process a user input string through the agentic loop.
29129
+ *
29130
+ * Appends a UserTurn, then repeatedly calls the LLM and executes tool calls
29131
+ * until natural completion (no tool calls) or a configured limit is hit.
29132
+ *
29133
+ * After natural completion, drains the follow-up queue (FIFO) by recursively
29134
+ * calling processInput for each queued message. PROCESSING_END is only emitted
29135
+ * when the follow-up queue is fully exhausted.
29136
+ */
29137
+ async processInput(userInput) {
29138
+ this.state = SessionState.PROCESSING;
29139
+ const userTurn = {
29140
+ type: "user",
29141
+ content: userInput,
29142
+ timestamp: new Date()
29143
+ };
29144
+ this.history.push(userTurn);
29145
+ this._emit(EventKind.USER_INPUT, { content: userInput });
29146
+ let roundCount = 0;
29147
+ const loopDetector = new LoopDetector({
29148
+ windowSize: this.config.loop_detection_window,
29149
+ enabled: this.config.enable_loop_detection
29150
+ });
29151
+ try {
29152
+ while (true) {
29153
+ const totalTurns = this.history.length;
29154
+ if (this.config.max_turns > 0 && totalTurns >= this.config.max_turns) {
29155
+ this._emit(EventKind.TURN_LIMIT, {
29156
+ total_turns: totalTurns,
29157
+ reason: "max_turns"
29158
+ });
29159
+ break;
29160
+ }
29161
+ if (this.config.max_tool_rounds_per_input > 0 && roundCount >= this.config.max_tool_rounds_per_input) {
29162
+ this._emit(EventKind.TURN_LIMIT, {
29163
+ round: roundCount,
29164
+ reason: "max_tool_rounds_per_input"
29165
+ });
29166
+ break;
29167
+ }
29168
+ if (this._abortController.signal.aborted) break;
29169
+ this._drainSteering();
29170
+ const response = await this.llmClient.complete(buildLLMRequest(this));
29171
+ const assistantTurn = {
29172
+ type: "assistant",
29173
+ content: response.content,
29174
+ tool_calls: response.toolCalls ?? [],
29175
+ reasoning: null,
29176
+ usage: response.usage,
29177
+ response_id: response.id ?? null,
29178
+ timestamp: new Date()
29179
+ };
29180
+ this.history.push(assistantTurn);
29181
+ this._emit(EventKind.ASSISTANT_TEXT_END, {
29182
+ text: response.content,
29183
+ reasoning: null
29184
+ });
29185
+ if (!response.toolCalls || response.toolCalls.length === 0) {
29186
+ if (this._followupQueue.length > 0) {
29187
+ const nextInput = this._followupQueue.shift();
29188
+ await this.processInput(nextInput);
29189
+ return;
29190
+ }
29191
+ break;
29192
+ }
29193
+ roundCount++;
29194
+ const toolResults = await this._executeToolCalls(response.toolCalls);
29195
+ const toolResultsTurn = {
29196
+ type: "tool_results",
29197
+ results: toolResults,
29198
+ timestamp: new Date()
29199
+ };
29200
+ this.history.push(toolResultsTurn);
29201
+ let loopTriggered = false;
29202
+ for (const tc of response.toolCalls) {
29203
+ const detected = loopDetector.record(tc.name, tc.arguments);
29204
+ if (detected) loopTriggered = true;
29205
+ }
29206
+ this._drainSteering();
29207
+ if (loopTriggered) {
29208
+ const warningMessage = `Loop detected: the last ${this.config.loop_detection_window} tool calls follow a repeating pattern. Try a different approach.`;
29209
+ const loopTurn = {
29210
+ type: "steering",
29211
+ content: warningMessage,
29212
+ timestamp: new Date()
29213
+ };
29214
+ this.history.push(loopTurn);
29215
+ this._emit(EventKind.LOOP_DETECTION, { message: warningMessage });
29216
+ }
29217
+ }
29218
+ this.state = SessionState.IDLE;
29219
+ this._emit(EventKind.PROCESSING_END, {});
29220
+ } catch (err) {
29221
+ const message = err instanceof Error ? err.message : String(err);
29222
+ this._emit(EventKind.ERROR, { message });
29223
+ this.state = SessionState.CLOSED;
29224
+ throw err;
29225
+ }
29226
+ }
29227
+ /** Dispatch tool calls in parallel or sequentially based on provider profile. */
29228
+ async _executeToolCalls(toolCalls) {
29229
+ if (this.providerProfile.supports_parallel_tool_calls && toolCalls.length > 1) return Promise.all(toolCalls.map((tc) => this._executeSingleTool(tc)));
29230
+ else {
29231
+ const results = [];
29232
+ for (const tc of toolCalls) results.push(await this._executeSingleTool(tc));
29233
+ return results;
29234
+ }
29235
+ }
29236
+ /** Execute a single tool call: lookup, emit events, delegate to registry, truncate. */
29237
+ async _executeSingleTool(toolCall) {
29238
+ this._emit(EventKind.TOOL_CALL_START, {
29239
+ tool_name: toolCall.name,
29240
+ call_id: toolCall.id
29241
+ });
29242
+ const toolDef = this._toolRegistry.get(toolCall.name);
29243
+ if (!toolDef) {
29244
+ const errorContent = `Unknown tool: ${toolCall.name}`;
29245
+ this._emit(EventKind.TOOL_CALL_END, {
29246
+ call_id: toolCall.id,
29247
+ output: errorContent,
29248
+ is_error: true
29249
+ });
29250
+ return {
29251
+ tool_call_id: toolCall.id,
29252
+ content: errorContent,
29253
+ is_error: true
29254
+ };
29255
+ }
29256
+ const toolResult = await this._toolRegistry.execute(toolCall.name, toolCall.arguments, this.executionEnv);
29257
+ const rawOutput = toolResult.content;
29258
+ const truncatedOutput = truncateToolOutput(rawOutput, toolCall.name, this.config);
29259
+ this._emit(EventKind.TOOL_CALL_END, {
29260
+ call_id: toolCall.id,
29261
+ output: rawOutput,
29262
+ is_error: toolResult.isError
29263
+ });
29264
+ return {
29265
+ tool_call_id: toolCall.id,
29266
+ content: truncatedOutput,
29267
+ is_error: toolResult.isError
29268
+ };
29269
+ }
29270
+ };
29271
+ /**
29272
+ * Create a new CodingAgentSession.
29273
+ * SESSION_START is emitted synchronously during construction.
29274
+ */
29275
+ function createSession(options) {
29276
+ return new CodingAgentSession(options);
29277
+ }
29278
+
29279
+ //#endregion
29280
+ //#region packages/factory/dist/backend/direct-backend.js
29281
+ var DirectCodergenBackend = class {
29282
+ options;
29283
+ constructor(options) {
29284
+ this.options = options;
29285
+ }
29286
+ async run(node, prompt, _context) {
29287
+ const { llmClient, providerProfile, executionEnv, config, onEvent } = this.options;
29288
+ const sessionOptions = {
29289
+ llmClient,
29290
+ providerProfile,
29291
+ executionEnv
29292
+ };
29293
+ if (config !== void 0) sessionOptions.config = config;
29294
+ const session = createSession(sessionOptions);
29295
+ let turnLimitHit = false;
29296
+ if (onEvent) for (const kind of Object.values(EventKind)) session.on(kind, onEvent);
29297
+ session.on(EventKind.TURN_LIMIT, () => {
29298
+ turnLimitHit = true;
29299
+ });
29300
+ try {
29301
+ await session.processInput(prompt);
29302
+ } catch (err) {
29303
+ const failureReason = err instanceof Error ? err.message : String(err);
29304
+ return {
29305
+ status: "FAILURE",
29306
+ failureReason
29307
+ };
29308
+ } finally {
29309
+ session.close();
29310
+ }
29311
+ if (turnLimitHit) return {
29312
+ status: "FAILURE",
29313
+ failureReason: "turn limit exceeded"
29314
+ };
29315
+ const finalAssistantTurn = [...session.history].reverse().find((t) => t.type === "assistant");
29316
+ if (!finalAssistantTurn || finalAssistantTurn.type !== "assistant") return { status: "SUCCESS" };
29317
+ return {
29318
+ status: "SUCCESS",
29319
+ contextUpdates: { [`${node.id}_output`]: finalAssistantTurn.content }
29320
+ };
29321
+ }
29322
+ };
29323
+ function createDirectCodergenBackend(options) {
29324
+ return new DirectCodergenBackend(options);
29325
+ }
29326
+
29327
+ //#endregion
29328
+ //#region packages/factory/dist/llm/model-registry.js
29329
+ const DEFAULT_PATTERNS = [
29330
+ {
29331
+ pattern: /^claude-/i,
29332
+ provider: "anthropic"
29333
+ },
29334
+ {
29335
+ pattern: /^gpt-/i,
29336
+ provider: "openai"
29337
+ },
29338
+ {
29339
+ pattern: /^o\d(-|$)/i,
29340
+ provider: "openai"
29341
+ },
29342
+ {
29343
+ pattern: /^gemini-/i,
29344
+ provider: "gemini"
29345
+ }
29346
+ ];
29347
+ var ModelRegistry = class {
29348
+ patterns = [...DEFAULT_PATTERNS];
29349
+ /**
29350
+ * Register a glob-style pattern (supports `*` wildcard) mapped to a provider name.
29351
+ * Custom patterns are prepended so they override defaults.
29352
+ */
29353
+ register(globPattern, provider) {
29354
+ const escaped = globPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
29355
+ this.patterns.unshift({
29356
+ pattern: new RegExp(`^${escaped}$`, "i"),
29357
+ provider
29358
+ });
29359
+ }
29360
+ /**
29361
+ * Resolve a model string to a provider name.
29362
+ * Returns `null` if no pattern matches.
29363
+ */
29364
+ resolve(model) {
29365
+ for (const { pattern, provider } of this.patterns) if (pattern.test(model)) return provider;
29366
+ return null;
29367
+ }
29368
+ };
29369
+
29370
+ //#endregion
29371
+ //#region packages/factory/dist/llm/middleware/types.js
29372
+ /**
29373
+ * Composes middleware into a single next-function.
29374
+ * First middleware in the array is the outermost wrapper (runs first / last to finish).
29375
+ * Implemented via reduceRight so the array order matches onion semantics.
29376
+ */
29377
+ function buildMiddlewareChain(middleware, base) {
29378
+ return middleware.reduceRight((next, mw) => (req) => mw(req, next), base);
29379
+ }
29380
+
29381
+ //#endregion
29382
+ //#region packages/factory/dist/llm/client.js
29383
+ var LLMClient = class {
29384
+ adapters = new Map();
29385
+ modelRegistry = new ModelRegistry();
29386
+ _middleware = [];
29387
+ constructor(adapters) {
29388
+ if (adapters) for (const [name, adapter] of Object.entries(adapters)) this.registerProvider(name, adapter);
29389
+ }
29390
+ registerProvider(name, adapter) {
29391
+ this.adapters.set(name, adapter);
29392
+ }
29393
+ registerModelPattern(pattern, providerName) {
29394
+ this.modelRegistry.register(pattern, providerName);
29395
+ }
29396
+ /**
29397
+ * Register a middleware function that wraps every `complete()` call.
29398
+ * Returns `this` for chaining.
29399
+ */
29400
+ use(mw) {
29401
+ this._middleware.push(mw);
29402
+ return this;
29403
+ }
29404
+ resolveAdapter(model) {
29405
+ const providerName = this.modelRegistry.resolve(model);
29406
+ const registered = [...this.adapters.keys()];
29407
+ if (!providerName) throw new Error(`No provider matched model "${model}". Registered providers: ${registered.join(", ") || "(none)"}`);
29408
+ const adapter = this.adapters.get(providerName);
29409
+ if (!adapter) throw new Error(`Provider "${providerName}" matched model "${model}" but is not registered. Registered providers: ${registered.join(", ") || "(none)"}`);
29410
+ return adapter;
29411
+ }
29412
+ async complete(request) {
29413
+ const baseNext = (req) => this.resolveAdapter(req.model).complete(req);
29414
+ const chain = buildMiddlewareChain(this._middleware, baseNext);
29415
+ return chain(request);
29416
+ }
29417
+ async *stream(request) {
29418
+ yield* this.resolveAdapter(request.model).stream(request);
29419
+ }
29420
+ };
29421
+
29422
+ //#endregion
29423
+ //#region packages/factory/dist/llm/types.js
29424
+ /**
29425
+ * Error thrown by provider adapters that includes the HTTP status code.
29426
+ * The retry middleware uses `statusCode` to determine if an error is retryable
29427
+ * (429, 500, 502, 503) vs non-retryable (400, 401, 403, 404).
29428
+ */
29429
+ var LLMError = class extends Error {
29430
+ statusCode;
29431
+ provider;
29432
+ constructor(message, statusCode, provider) {
29433
+ super(message);
29434
+ this.statusCode = statusCode;
29435
+ this.provider = provider;
29436
+ this.name = "LLMError";
29437
+ }
29438
+ };
29439
+
29440
+ //#endregion
29441
+ //#region packages/factory/dist/llm/providers/anthropic.js
29442
+ const MAX_RETRIES = 3;
29443
+ const BASE_DELAY_MS = 1e3;
29444
+ const DEFAULT_MAX_TOKENS = 4096;
29445
+ const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
29446
+ const PROMPT_CACHING_BETA = "prompt-caching-2024-07-31";
29447
+ function mapStopReason(raw) {
29448
+ switch (raw) {
29449
+ case "end_turn":
29450
+ case "stop_sequence": return "stop";
29451
+ case "max_tokens": return "length";
29452
+ case "tool_use": return "tool_calls";
29453
+ default: return "other";
29454
+ }
29455
+ }
29456
+ function reasoningBudget(effort) {
29457
+ switch (effort) {
29458
+ case "low": return 1024;
29459
+ case "medium": return 8192;
29460
+ case "high": return 32e3;
29461
+ }
29462
+ }
29463
+ var AnthropicAdapter = class {
29464
+ name = "anthropic";
29465
+ apiKey;
29466
+ baseUrl;
29467
+ anthropicVersion;
29468
+ fetch;
29469
+ constructor(options) {
29470
+ this.apiKey = options.apiKey;
29471
+ this.baseUrl = options.baseUrl ?? "https://api.anthropic.com";
29472
+ this.anthropicVersion = options.anthropicVersion ?? DEFAULT_ANTHROPIC_VERSION;
29473
+ this.fetch = options.fetch ?? globalThis.fetch;
29474
+ }
29475
+ async complete(request) {
29476
+ const { body, betaHeaders } = this.buildRequestBody(request);
29477
+ const headers = this.buildHeaders(betaHeaders);
29478
+ const url = `${this.baseUrl}/v1/messages`;
29479
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
29480
+ const response = await this.fetch(url, {
29481
+ method: "POST",
29482
+ headers,
29483
+ body: JSON.stringify(body)
29484
+ });
29485
+ if (response.status === 429) {
29486
+ if (attempt === MAX_RETRIES) throw new LLMError(`[anthropic] Rate limit exceeded after ${MAX_RETRIES} retries`, 429, "anthropic");
29487
+ const retryAfterStr = response.headers.get("Retry-After");
29488
+ const retryAfter = retryAfterStr !== null ? parseInt(retryAfterStr, 10) * 1e3 : 0;
29489
+ const backoff = Math.max(retryAfter, BASE_DELAY_MS * 2 ** attempt);
29490
+ await new Promise((resolve$6) => setTimeout(resolve$6, backoff));
29491
+ continue;
29492
+ }
29493
+ if (!response.ok) {
29494
+ const errorBody = await response.json().catch(() => ({}));
29495
+ const errorObj = errorBody?.error;
29496
+ const message = typeof errorObj?.message === "string" ? errorObj.message : response.statusText;
29497
+ throw new LLMError(`[anthropic] ${response.status}: ${message}`, response.status, "anthropic");
29498
+ }
29499
+ const raw = await response.json();
29500
+ return this.parseResponse(raw, request.model);
29501
+ }
29502
+ throw new Error("[anthropic] Unexpected end of retry loop");
29503
+ }
29504
+ async *stream(_request) {
29505
+ throw new Error("streaming not yet implemented");
29506
+ }
29507
+ buildRequestBody(request) {
29508
+ const betaHeaders = [];
29509
+ const anthropicExtra = request.extra?.anthropic;
29510
+ const autoCache = anthropicExtra !== void 0 ? anthropicExtra["auto_cache"] !== false : true;
29511
+ const extraBetas = anthropicExtra?.["beta_headers"];
29512
+ if (Array.isArray(extraBetas)) for (const b of extraBetas) betaHeaders.push(b);
29513
+ let systemBlocks;
29514
+ if (request.systemPrompt) {
29515
+ const block = {
29516
+ type: "text",
29517
+ text: request.systemPrompt
29518
+ };
29519
+ systemBlocks = [block];
29520
+ }
29521
+ const messages = this.translateMessages(request.messages);
29522
+ const toolChoiceIsNone = request.toolChoice === "none";
29523
+ let tools;
29524
+ let toolChoice;
29525
+ if (!toolChoiceIsNone && request.tools && request.tools.length > 0) {
29526
+ tools = this.translateTools(request.tools);
29527
+ toolChoice = this.mapToolChoice(request.toolChoice);
29528
+ }
29529
+ let cachingInjected = false;
29530
+ if (autoCache) {
29531
+ if (systemBlocks && systemBlocks.length > 0) {
29532
+ const lastBlock = systemBlocks[systemBlocks.length - 1];
29533
+ if (lastBlock && !("cache_control" in lastBlock && lastBlock.cache_control)) {
29534
+ lastBlock.cache_control = { type: "ephemeral" };
29535
+ cachingInjected = true;
29536
+ }
29537
+ }
29538
+ if (tools && tools.length > 0) {
29539
+ const lastTool = tools[tools.length - 1];
29540
+ if (lastTool && !lastTool.cache_control) {
29541
+ lastTool.cache_control = { type: "ephemeral" };
29542
+ cachingInjected = true;
29543
+ }
29544
+ }
29545
+ }
29546
+ if (cachingInjected && !betaHeaders.includes(PROMPT_CACHING_BETA)) betaHeaders.push(PROMPT_CACHING_BETA);
29547
+ const body = {
29548
+ model: request.model,
29549
+ max_tokens: request.maxTokens ?? DEFAULT_MAX_TOKENS,
29550
+ messages
29551
+ };
29552
+ if (systemBlocks) body.system = systemBlocks;
29553
+ if (tools) body.tools = tools;
29554
+ if (toolChoice) body.tool_choice = toolChoice;
29555
+ if (request.temperature !== void 0) body.temperature = request.temperature;
29556
+ if (request.reasoningEffort) body.thinking = {
29557
+ type: "enabled",
29558
+ budget_tokens: reasoningBudget(request.reasoningEffort)
29559
+ };
29560
+ return {
29561
+ body,
29562
+ betaHeaders
29563
+ };
29564
+ }
29565
+ buildHeaders(betaHeaders) {
29566
+ const headers = {
29567
+ "Content-Type": "application/json",
29568
+ "x-api-key": this.apiKey,
29569
+ "anthropic-version": this.anthropicVersion
29570
+ };
29571
+ if (betaHeaders.length > 0) headers["anthropic-beta"] = betaHeaders.join(",");
29572
+ return headers;
29573
+ }
29574
+ translateMessages(messages) {
29575
+ const filtered = messages.filter((m) => m.role !== "system");
29576
+ const translated = filtered.map((msg) => {
29577
+ if (msg.role === "tool") {
29578
+ const content$1 = msg.content.map((part) => {
29579
+ if (part.toolResult) return {
29580
+ type: "tool_result",
29581
+ tool_use_id: part.toolResult.toolCallId,
29582
+ content: part.toolResult.content,
29583
+ is_error: part.toolResult.isError
29584
+ };
29585
+ return {
29586
+ type: "text",
29587
+ text: part.text ?? ""
29588
+ };
29589
+ });
29590
+ return {
29591
+ role: "user",
29592
+ content: content$1
29593
+ };
29594
+ }
29595
+ const role = msg.role;
29596
+ const content = msg.content.map((part) => {
29597
+ if (part.kind === "text") return {
29598
+ type: "text",
29599
+ text: part.text ?? ""
29600
+ };
29601
+ if (part.kind === "tool_call" && part.toolCall) return {
29602
+ type: "tool_use",
29603
+ id: part.toolCall.id,
29604
+ name: part.toolCall.name,
29605
+ input: part.toolCall.arguments
29606
+ };
29607
+ if (part.kind === "tool_result" && part.toolResult) return {
29608
+ type: "tool_result",
29609
+ tool_use_id: part.toolResult.toolCallId,
29610
+ content: part.toolResult.content,
29611
+ is_error: part.toolResult.isError
29612
+ };
29613
+ return {
29614
+ type: "text",
29615
+ text: part.text ?? ""
29616
+ };
29617
+ });
29618
+ return {
29619
+ role,
29620
+ content
29621
+ };
29622
+ });
29623
+ const merged = [];
29624
+ for (const msg of translated) {
29625
+ const last = merged[merged.length - 1];
29626
+ if (last !== void 0 && last.role === msg.role) last.content = [...last.content, ...msg.content];
29627
+ else merged.push({
29628
+ role: msg.role,
29629
+ content: [...msg.content]
29630
+ });
29631
+ }
29632
+ return merged;
29633
+ }
29634
+ translateTools(tools) {
29635
+ return tools.map((tool) => ({
29636
+ name: tool.name,
29637
+ description: tool.description,
29638
+ input_schema: tool.parameters
29639
+ }));
29640
+ }
29641
+ mapToolChoice(toolChoice) {
29642
+ if (!toolChoice || toolChoice === "none") return void 0;
29643
+ if (toolChoice === "auto") return { type: "auto" };
29644
+ if (toolChoice === "required") return { type: "any" };
29645
+ if (typeof toolChoice === "object" && toolChoice.type === "function") return {
29646
+ type: "tool",
29647
+ name: toolChoice.name
29648
+ };
29649
+ return void 0;
29650
+ }
29651
+ parseResponse(raw, model) {
29652
+ let content = "";
29653
+ const toolCalls = [];
29654
+ for (const block of raw.content) if (block.type === "text") content += block.text;
29655
+ else if (block.type === "tool_use") toolCalls.push({
29656
+ id: block.id,
29657
+ name: block.name,
29658
+ arguments: block.input,
29659
+ rawArguments: JSON.stringify(block.input)
29660
+ });
29661
+ const stopReason = mapStopReason(raw.stop_reason);
29662
+ const usage = {
29663
+ inputTokens: raw.usage.input_tokens,
29664
+ outputTokens: raw.usage.output_tokens,
29665
+ totalTokens: raw.usage.input_tokens + raw.usage.output_tokens,
29666
+ ...raw.usage.cache_read_input_tokens !== void 0 ? { cacheReadTokens: raw.usage.cache_read_input_tokens } : {},
29667
+ ...raw.usage.cache_creation_input_tokens !== void 0 ? { cacheWriteTokens: raw.usage.cache_creation_input_tokens } : {}
29668
+ };
29669
+ return {
29670
+ content,
29671
+ toolCalls,
29672
+ usage,
29673
+ model: raw.model ?? model,
29674
+ stopReason,
29675
+ providerMetadata: { raw },
29676
+ id: raw.id
29677
+ };
29678
+ }
29679
+ };
29680
+
29681
+ //#endregion
29682
+ //#region packages/factory/dist/llm/providers/openai.js
29683
+ var OpenAIAdapter = class {
29684
+ name = "openai";
29685
+ apiKey;
29686
+ baseUrl;
29687
+ orgId;
29688
+ projectId;
29689
+ timeout;
29690
+ constructor(options = {}) {
29691
+ const apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"];
29692
+ if (!apiKey) throw new Error("OPENAI_API_KEY is required");
29693
+ this.apiKey = apiKey;
29694
+ const rawBaseUrl = options.baseUrl ?? process.env["OPENAI_BASE_URL"] ?? "https://api.openai.com/v1";
29695
+ this.baseUrl = rawBaseUrl.replace(/\/$/, "");
29696
+ this.orgId = options.orgId ?? process.env["OPENAI_ORG_ID"];
29697
+ this.projectId = options.projectId ?? process.env["OPENAI_PROJECT_ID"];
29698
+ this.timeout = options.timeout ?? 3e5;
29699
+ }
29700
+ _defaultHeaders() {
29701
+ const headers = {
29702
+ Authorization: `Bearer ${this.apiKey}`,
29703
+ "Content-Type": "application/json"
29704
+ };
29705
+ if (this.orgId) headers["OpenAI-Organization"] = this.orgId;
29706
+ if (this.projectId) headers["OpenAI-Project"] = this.projectId;
29707
+ return headers;
29708
+ }
29709
+ _translateContentParts(content, role) {
29710
+ const parts = [];
29711
+ for (const part of content) if (part.kind === "text" && part.text !== void 0) if (role === "user") parts.push({
29712
+ type: "input_text",
29713
+ text: part.text
29714
+ });
29715
+ else parts.push({
29716
+ type: "output_text",
29717
+ text: part.text
29718
+ });
29719
+ else if (part.kind === "tool_call" && part.toolCall) parts.push({
29720
+ type: "function_call",
29721
+ call_id: part.toolCall.id,
29722
+ name: part.toolCall.name,
29723
+ arguments: part.toolCall.rawArguments ?? JSON.stringify(part.toolCall.arguments)
29724
+ });
29725
+ return parts;
29726
+ }
29727
+ _translateMessages(request, body) {
29728
+ if (request.systemPrompt) body.instructions = request.systemPrompt;
29729
+ const input = [];
29730
+ for (const msg of request.messages) {
29731
+ if (msg.role === "system") continue;
29732
+ if (msg.role === "user") input.push({
29733
+ type: "message",
29734
+ role: "user",
29735
+ content: this._translateContentParts(msg.content, "user")
29736
+ });
29737
+ else if (msg.role === "assistant") input.push({
29738
+ type: "message",
29739
+ role: "assistant",
29740
+ content: this._translateContentParts(msg.content, "assistant")
29741
+ });
29742
+ else if (msg.role === "tool") {
29743
+ const callId = msg.toolCallId ?? this._extractToolCallId(msg);
29744
+ const output = this._extractToolOutput(msg);
29745
+ input.push({
29746
+ type: "function_call_output",
29747
+ call_id: callId,
29748
+ output
29749
+ });
29750
+ }
29751
+ }
29752
+ body.input = input;
29753
+ }
29754
+ _extractToolCallId(msg) {
29755
+ for (const part of msg.content) if (part.kind === "tool_result" && part.toolResult?.toolCallId) return part.toolResult.toolCallId;
29756
+ return "";
29757
+ }
29758
+ _extractToolOutput(msg) {
29759
+ for (const part of msg.content) if (part.kind === "text" && part.text !== void 0) return part.text;
29760
+ for (const part of msg.content) if (part.kind === "tool_result" && part.toolResult?.content !== void 0) return String(part.toolResult.content);
29761
+ return "";
29762
+ }
29763
+ _translateTools(request, body) {
29764
+ if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
29765
+ type: "function",
29766
+ function: {
29767
+ name: t.name,
29768
+ description: t.description,
29769
+ parameters: t.parameters
29770
+ }
29771
+ }));
29772
+ if (request.toolChoice !== void 0) {
29773
+ if (typeof request.toolChoice === "string") body.tool_choice = request.toolChoice;
29774
+ else if (request.toolChoice.type === "function") body.tool_choice = {
29775
+ type: "function",
29776
+ function: { name: request.toolChoice.name }
29777
+ };
29778
+ }
29779
+ }
29780
+ _parseUsage(usage) {
29781
+ const inputTokens = usage.input_tokens;
29782
+ const outputTokens = usage.output_tokens;
29783
+ const totalTokens = inputTokens + outputTokens;
29784
+ const result = {
29785
+ inputTokens,
29786
+ outputTokens,
29787
+ totalTokens
29788
+ };
29789
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens;
29790
+ if (reasoningTokens !== void 0 && reasoningTokens > 0) result.reasoningTokens = reasoningTokens;
29791
+ const cacheReadTokens = usage.input_tokens_details?.cached_tokens;
29792
+ if (cacheReadTokens !== void 0 && cacheReadTokens > 0) result.cacheReadTokens = cacheReadTokens;
29793
+ return result;
29794
+ }
29795
+ _mapStopReason(response, hasToolCalls) {
29796
+ if (response.status === "completed") return hasToolCalls ? "tool_calls" : "stop";
29797
+ if (response.status === "incomplete") {
29798
+ const reason = response.incomplete_details?.reason;
29799
+ if (reason === "max_output_tokens") return "length";
29800
+ if (reason === "content_filter") return "content_filter";
29801
+ }
29802
+ return "other";
29803
+ }
29804
+ async complete(request) {
29805
+ const body = {
29806
+ model: request.model,
29807
+ input: []
29808
+ };
29809
+ this._translateMessages(request, body);
29810
+ this._translateTools(request, body);
29811
+ if (request.maxTokens !== void 0) body.max_output_tokens = request.maxTokens;
29812
+ if (request.temperature !== void 0) body.temperature = request.temperature;
29813
+ if (request.reasoningEffort !== void 0) body.reasoning = { effort: request.reasoningEffort };
29814
+ if (request.extra?.openai) Object.assign(body, request.extra.openai);
29815
+ const controller = new AbortController();
29816
+ const timer = setTimeout(() => {
29817
+ controller.abort();
29818
+ }, this.timeout);
29819
+ let rawResponse;
29820
+ try {
29821
+ rawResponse = await fetch(`${this.baseUrl}/responses`, {
29822
+ method: "POST",
29823
+ headers: this._defaultHeaders(),
29824
+ body: JSON.stringify(body),
29825
+ signal: controller.signal
29826
+ });
29827
+ } finally {
29828
+ clearTimeout(timer);
29829
+ }
29830
+ if (!rawResponse.ok) {
29831
+ let errorMessage = `OpenAI API error: ${rawResponse.status}`;
29832
+ try {
29833
+ const errorBody = await rawResponse.json();
29834
+ if (errorBody.error?.message) errorMessage = `OpenAI API error ${rawResponse.status}: ${errorBody.error.message}`;
29835
+ } catch {}
29836
+ throw new LLMError(errorMessage, rawResponse.status, "openai");
29837
+ }
29838
+ const data = await rawResponse.json();
29839
+ let content = "";
29840
+ const toolCalls = [];
29841
+ for (const item of data.output) if (item.type === "message") {
29842
+ for (const part of item.content) if (part.type === "output_text") content += part.text;
29843
+ } else if (item.type === "function_call") {
29844
+ let parsedArgs = {};
29845
+ try {
29846
+ parsedArgs = JSON.parse(item.arguments);
29847
+ } catch {}
29848
+ toolCalls.push({
29849
+ id: item.call_id,
29850
+ name: item.name,
29851
+ arguments: parsedArgs,
29852
+ rawArguments: item.arguments
29853
+ });
29854
+ }
29855
+ const stopReason = this._mapStopReason(data, toolCalls.length > 0);
29856
+ const usage = this._parseUsage(data.usage);
29857
+ return {
29858
+ content,
29859
+ toolCalls,
29860
+ usage,
29861
+ model: data.model,
29862
+ stopReason,
29863
+ providerMetadata: { id: data.id },
29864
+ id: data.id
29865
+ };
29866
+ }
29867
+ async *stream(request) {
29868
+ const body = {
29869
+ model: request.model,
29870
+ input: [],
29871
+ stream: true
29872
+ };
29873
+ this._translateMessages(request, body);
29874
+ this._translateTools(request, body);
29875
+ if (request.maxTokens !== void 0) body.max_output_tokens = request.maxTokens;
29876
+ if (request.temperature !== void 0) body.temperature = request.temperature;
29877
+ if (request.reasoningEffort !== void 0) body.reasoning = { effort: request.reasoningEffort };
29878
+ if (request.extra) Object.assign(body, request.extra);
29879
+ const controller = new AbortController();
29880
+ const timer = setTimeout(() => {
29881
+ controller.abort();
29882
+ }, this.timeout);
29883
+ let rawResponse;
29884
+ try {
29885
+ rawResponse = await fetch(`${this.baseUrl}/responses`, {
29886
+ method: "POST",
29887
+ headers: this._defaultHeaders(),
29888
+ body: JSON.stringify(body),
29889
+ signal: controller.signal
29890
+ });
29891
+ } catch (err) {
29892
+ clearTimeout(timer);
29893
+ throw err;
29894
+ }
29895
+ if (!rawResponse.ok) {
29896
+ clearTimeout(timer);
29897
+ let errorMessage = `OpenAI API error: ${rawResponse.status}`;
29898
+ try {
29899
+ const errorBody = await rawResponse.json();
29900
+ if (errorBody.error?.message) errorMessage = `OpenAI API error ${rawResponse.status}: ${errorBody.error.message}`;
29901
+ } catch {}
29902
+ throw new LLMError(errorMessage, rawResponse.status, "openai");
29903
+ }
29904
+ try {
29905
+ const text = await rawResponse.text();
29906
+ const lines = text.split("\n");
29907
+ let started = false;
29908
+ let currentEventName = "";
29909
+ for (const line of lines) {
29910
+ const trimmed = line.trim();
29911
+ if (trimmed.startsWith("event: ")) currentEventName = trimmed.slice(7).trim();
29912
+ else if (trimmed.startsWith("data: ")) {
29913
+ const dataStr = trimmed.slice(6).trim();
29914
+ if (dataStr === "[DONE]") break;
29915
+ let eventData;
29916
+ try {
29917
+ eventData = JSON.parse(dataStr);
29918
+ } catch {
29919
+ continue;
29920
+ }
29921
+ if (!started) {
29922
+ started = true;
29923
+ yield { type: "message_start" };
29924
+ }
29925
+ if (currentEventName === "response.output_text.delta") yield {
29926
+ type: "text_delta",
29927
+ delta: eventData["delta"] ?? ""
29928
+ };
29929
+ else if (currentEventName === "response.function_call_arguments.delta") {
29930
+ const itemId = eventData["item_id"];
29931
+ const toolCallDelta = { rawArguments: eventData["delta"] ?? "" };
29932
+ if (itemId !== void 0) toolCallDelta.id = itemId;
29933
+ yield {
29934
+ type: "tool_call_delta",
29935
+ toolCall: toolCallDelta
29936
+ };
29937
+ } else if (currentEventName === "response.output_item.done") {
29938
+ const itemType = eventData["type"];
29939
+ if (itemType === "function_call") {
29940
+ const rawArgs = eventData["arguments"] ?? "";
29941
+ let parsedArgs = {};
29942
+ try {
29943
+ parsedArgs = JSON.parse(rawArgs);
29944
+ } catch {}
29945
+ const callId = eventData["call_id"];
29946
+ const callName = eventData["name"];
29947
+ const doneToolCall = { arguments: parsedArgs };
29948
+ if (callId !== void 0) doneToolCall.id = callId;
29949
+ if (callName !== void 0) doneToolCall.name = callName;
29950
+ yield {
29951
+ type: "tool_call_delta",
29952
+ toolCall: doneToolCall
29953
+ };
29954
+ }
29955
+ } else if (currentEventName === "response.completed" || currentEventName === "response.created") {
29956
+ if (currentEventName === "response.completed") {
29957
+ const completedResponse = eventData["response"] ?? eventData;
29958
+ const hasToolCalls = Array.isArray(completedResponse.output) && completedResponse.output.some((o) => o.type === "function_call");
29959
+ const stopReason = this._mapStopReason(completedResponse, hasToolCalls);
29960
+ yield {
29961
+ type: "message_stop",
29962
+ usage: this._parseUsage(completedResponse.usage),
29963
+ finishReason: { reason: stopReason }
29964
+ };
29965
+ }
29966
+ }
29967
+ }
29968
+ }
29969
+ } finally {
29970
+ clearTimeout(timer);
29971
+ }
29972
+ }
29973
+ };
29974
+
29975
+ //#endregion
29976
+ //#region packages/factory/dist/llm/providers/gemini.js
29977
+ var GeminiAdapter = class {
29978
+ name = "gemini";
29979
+ apiKey;
29980
+ baseUrl;
29981
+ timeout;
29982
+ constructor(options = {}) {
29983
+ const apiKey = options.apiKey ?? process.env["GEMINI_API_KEY"] ?? process.env["GOOGLE_API_KEY"];
29984
+ if (!apiKey) throw new Error("GEMINI_API_KEY is required");
29985
+ this.apiKey = apiKey;
29986
+ const rawBaseUrl = options.baseUrl ?? process.env["GEMINI_BASE_URL"] ?? "https://generativelanguage.googleapis.com";
29987
+ this.baseUrl = rawBaseUrl.replace(/\/$/, "");
29988
+ this.timeout = options.timeout ?? 3e5;
29989
+ }
29990
+ _buildUrl(model, streaming) {
29991
+ const action = streaming ? "streamGenerateContent" : "generateContent";
29992
+ let url = `${this.baseUrl}/v1beta/models/${model}:${action}?key=${this.apiKey}`;
29993
+ if (streaming) url += "&alt=sse";
29994
+ return url;
29995
+ }
29996
+ _wrapResponse(content) {
29997
+ try {
29998
+ const parsed = JSON.parse(content);
29999
+ if (typeof parsed === "object" && parsed !== null) return parsed;
30000
+ } catch {}
30001
+ return { result: content };
30002
+ }
30003
+ _translateParts(content) {
30004
+ const parts = [];
30005
+ for (const part of content) if (part.kind === "text" && part.text !== void 0) parts.push({ text: part.text });
30006
+ else if (part.kind === "tool_call" && part.toolCall) parts.push({ functionCall: {
30007
+ name: part.toolCall.name,
30008
+ args: part.toolCall.arguments
30009
+ } });
30010
+ return parts;
30011
+ }
30012
+ /**
30013
+ * Scan all messages to build a mapping from synthetic tool call IDs to function names.
30014
+ * This is needed to translate tool-result messages back into Gemini's functionResponse format.
30015
+ */
30016
+ _buildToolCallIdMap(messages) {
30017
+ const idToName = new Map();
30018
+ for (const msg of messages) if (msg.role === "assistant") {
30019
+ for (const part of msg.content) if (part.kind === "tool_call" && part.toolCall) idToName.set(part.toolCall.id, part.toolCall.name);
30020
+ }
30021
+ return idToName;
30022
+ }
30023
+ _translateMessages(request, body) {
30024
+ if (request.systemPrompt) body.systemInstruction = { parts: [{ text: request.systemPrompt }] };
30025
+ const idToName = this._buildToolCallIdMap(request.messages);
30026
+ const contents = [];
30027
+ for (const msg of request.messages) {
30028
+ if (msg.role === "system") continue;
30029
+ if (msg.role === "user") contents.push({
30030
+ role: "user",
30031
+ parts: this._translateParts(msg.content)
30032
+ });
30033
+ else if (msg.role === "assistant") contents.push({
30034
+ role: "model",
30035
+ parts: this._translateParts(msg.content)
30036
+ });
30037
+ else if (msg.role === "tool") {
30038
+ const toolResultParts = [];
30039
+ for (const part of msg.content) if (part.kind === "tool_result" && part.toolResult) {
30040
+ const { toolCallId, content } = part.toolResult;
30041
+ const functionName = idToName.get(toolCallId) ?? toolCallId;
30042
+ toolResultParts.push({ functionResponse: {
30043
+ name: functionName,
30044
+ response: this._wrapResponse(String(content))
30045
+ } });
30046
+ }
30047
+ if (toolResultParts.length === 0) {
30048
+ const callId = msg.toolCallId ?? "";
30049
+ const functionName = idToName.get(callId) ?? callId;
30050
+ const textContent = msg.content.filter((p) => p.kind === "text").map((p) => p.text ?? "").join("");
30051
+ toolResultParts.push({ functionResponse: {
30052
+ name: functionName,
30053
+ response: this._wrapResponse(textContent)
30054
+ } });
30055
+ }
30056
+ contents.push({
30057
+ role: "user",
30058
+ parts: toolResultParts
30059
+ });
30060
+ }
30061
+ }
30062
+ body.contents = contents;
30063
+ }
30064
+ _translateTools(request, body) {
30065
+ if (request.tools && request.tools.length > 0) body.tools = [{ functionDeclarations: request.tools.map((t) => ({
30066
+ name: t.name,
30067
+ description: t.description,
30068
+ parameters: t.parameters
30069
+ })) }];
30070
+ if (request.toolChoice !== void 0) {
30071
+ if (request.toolChoice === "auto") body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
30072
+ else if (request.toolChoice === "none") body.toolConfig = { functionCallingConfig: { mode: "NONE" } };
30073
+ else if (request.toolChoice === "required") body.toolConfig = { functionCallingConfig: { mode: "ANY" } };
30074
+ else if (typeof request.toolChoice === "object" && request.toolChoice.type === "function") body.toolConfig = { functionCallingConfig: {
30075
+ mode: "ANY",
30076
+ allowedFunctionNames: [request.toolChoice.name]
30077
+ } };
30078
+ }
30079
+ }
30080
+ _parseUsage(usageMetadata) {
30081
+ const inputTokens = usageMetadata.promptTokenCount ?? 0;
30082
+ const outputTokens = usageMetadata.candidatesTokenCount ?? 0;
30083
+ const totalTokens = inputTokens + outputTokens;
30084
+ const result = {
30085
+ inputTokens,
30086
+ outputTokens,
30087
+ totalTokens
30088
+ };
30089
+ if (usageMetadata.thoughtsTokenCount !== void 0 && usageMetadata.thoughtsTokenCount > 0) result.reasoningTokens = usageMetadata.thoughtsTokenCount;
30090
+ if (usageMetadata.cachedContentTokenCount !== void 0 && usageMetadata.cachedContentTokenCount > 0) result.cacheReadTokens = usageMetadata.cachedContentTokenCount;
30091
+ return result;
30092
+ }
30093
+ _mapStopReason(finishReason, hasToolCalls) {
30094
+ if (hasToolCalls) return "tool_calls";
30095
+ switch (finishReason) {
30096
+ case "STOP": return "stop";
30097
+ case "MAX_TOKENS": return "length";
30098
+ case "SAFETY":
30099
+ case "RECITATION": return "content_filter";
30100
+ default: return "other";
30101
+ }
30102
+ }
30103
+ async complete(request) {
30104
+ const body = { contents: [] };
30105
+ this._translateMessages(request, body);
30106
+ this._translateTools(request, body);
30107
+ const generationConfig = {};
30108
+ if (request.maxTokens !== void 0) generationConfig.maxOutputTokens = request.maxTokens;
30109
+ if (request.temperature !== void 0) generationConfig.temperature = request.temperature;
30110
+ if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
30111
+ if (request.extra?.gemini) Object.assign(body, request.extra.gemini);
30112
+ const controller = new AbortController();
30113
+ const timer = setTimeout(() => {
30114
+ controller.abort();
30115
+ }, this.timeout);
30116
+ let rawResponse;
30117
+ try {
30118
+ rawResponse = await fetch(this._buildUrl(request.model, false), {
30119
+ method: "POST",
30120
+ headers: { "Content-Type": "application/json" },
30121
+ body: JSON.stringify(body),
30122
+ signal: controller.signal
30123
+ });
30124
+ } finally {
30125
+ clearTimeout(timer);
30126
+ }
30127
+ if (!rawResponse.ok) {
30128
+ let errorMessage = `[gemini] ${rawResponse.status}`;
30129
+ try {
30130
+ const errorBody = await rawResponse.json();
30131
+ if (errorBody.error?.message) errorMessage = `[gemini] ${rawResponse.status}: ${errorBody.error.message}`;
30132
+ } catch {}
30133
+ throw new LLMError(errorMessage, rawResponse.status, "gemini");
30134
+ }
30135
+ const data = await rawResponse.json();
30136
+ const candidate = data.candidates?.[0];
30137
+ const parts = candidate?.content?.parts ?? [];
30138
+ const syntheticIdMap = new Map();
30139
+ const toolCalls = [];
30140
+ let content = "";
30141
+ for (const part of parts) if ("text" in part && part.text !== void 0) {
30142
+ const thoughtPart = part;
30143
+ if (!thoughtPart.thought) content += thoughtPart.text;
30144
+ } else if ("functionCall" in part) {
30145
+ const synthId = `call_${randomUUID()}`;
30146
+ syntheticIdMap.set(synthId, part.functionCall.name);
30147
+ toolCalls.push({
30148
+ id: synthId,
30149
+ name: part.functionCall.name,
30150
+ arguments: part.functionCall.args ?? {},
30151
+ rawArguments: JSON.stringify(part.functionCall.args)
30152
+ });
30153
+ }
30154
+ const stopReason = this._mapStopReason(candidate?.finishReason, toolCalls.length > 0);
30155
+ const usage = this._parseUsage(data.usageMetadata ?? {
30156
+ promptTokenCount: 0,
30157
+ candidatesTokenCount: 0
30158
+ });
30159
+ return {
30160
+ content,
30161
+ toolCalls,
30162
+ usage,
30163
+ model: request.model,
30164
+ stopReason,
30165
+ providerMetadata: { model: data.modelVersion ?? request.model }
30166
+ };
30167
+ }
30168
+ async *stream(request) {
30169
+ const body = { contents: [] };
30170
+ this._translateMessages(request, body);
30171
+ this._translateTools(request, body);
30172
+ const generationConfig = {};
30173
+ if (request.maxTokens !== void 0) generationConfig.maxOutputTokens = request.maxTokens;
30174
+ if (request.temperature !== void 0) generationConfig.temperature = request.temperature;
30175
+ if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
30176
+ if (request.extra) Object.assign(body, request.extra);
30177
+ const controller = new AbortController();
30178
+ const timer = setTimeout(() => {
30179
+ controller.abort();
30180
+ }, this.timeout);
30181
+ let rawResponse;
30182
+ try {
30183
+ rawResponse = await fetch(this._buildUrl(request.model, true), {
30184
+ method: "POST",
30185
+ headers: { "Content-Type": "application/json" },
30186
+ body: JSON.stringify(body),
30187
+ signal: controller.signal
30188
+ });
30189
+ } catch (err) {
30190
+ clearTimeout(timer);
30191
+ throw err;
30192
+ }
30193
+ if (!rawResponse.ok) {
30194
+ clearTimeout(timer);
30195
+ let errorMessage = `[gemini] ${rawResponse.status}`;
30196
+ try {
30197
+ const errorBody = await rawResponse.json();
30198
+ if (errorBody.error?.message) errorMessage = `[gemini] ${rawResponse.status}: ${errorBody.error.message}`;
30199
+ } catch {}
30200
+ throw new LLMError(errorMessage, rawResponse.status, "gemini");
30201
+ }
30202
+ try {
30203
+ const text = await rawResponse.text();
30204
+ const lines = text.split("\n");
30205
+ let started = false;
30206
+ for (const line of lines) {
30207
+ const trimmed = line.trim();
30208
+ if (!trimmed.startsWith("data: ")) continue;
30209
+ const dataStr = trimmed.slice(6).trim();
30210
+ if (dataStr === "[DONE]") break;
30211
+ let chunk;
30212
+ try {
30213
+ chunk = JSON.parse(dataStr);
30214
+ } catch {
30215
+ continue;
30216
+ }
30217
+ if (!started) {
30218
+ started = true;
30219
+ yield { type: "message_start" };
30220
+ }
30221
+ const candidateParts = chunk.candidates?.[0]?.content?.parts ?? [];
30222
+ for (const part of candidateParts) if ("text" in part && part.text !== void 0) {
30223
+ const thoughtPart = part;
30224
+ if (!thoughtPart.thought) yield {
30225
+ type: "text_delta",
30226
+ delta: thoughtPart.text
30227
+ };
30228
+ } else if ("functionCall" in part) {
30229
+ const synthId = `call_${randomUUID()}`;
30230
+ yield {
30231
+ type: "tool_call_delta",
30232
+ toolCall: {
30233
+ id: synthId,
30234
+ name: part.functionCall.name,
30235
+ arguments: part.functionCall.args ?? {}
30236
+ }
30237
+ };
30238
+ }
30239
+ if (chunk.usageMetadata) {
30240
+ const finishReason = chunk.candidates?.[0]?.finishReason;
30241
+ const hasToolCalls = candidateParts.some((p) => "functionCall" in p);
30242
+ const stopReason = this._mapStopReason(finishReason, hasToolCalls);
30243
+ yield {
30244
+ type: "message_stop",
30245
+ usage: this._parseUsage(chunk.usageMetadata),
30246
+ finishReason: { reason: stopReason }
30247
+ };
30248
+ }
30249
+ }
30250
+ } finally {
30251
+ clearTimeout(timer);
30252
+ }
30253
+ }
30254
+ };
30255
+
30256
+ //#endregion
30257
+ //#region packages/factory/dist/agent/tools/shared.js
30258
+ /**
30259
+ * Creates the five shared tools: read_file, write_file, shell, grep, glob.
30260
+ */
30261
+ function createSharedTools(shellTimeoutMs = 1e4) {
30262
+ return [
30263
+ createReadFileTool(),
30264
+ createWriteFileTool(),
30265
+ createShellTool(shellTimeoutMs),
30266
+ createGrepTool(shellTimeoutMs),
30267
+ createGlobTool()
30268
+ ];
30269
+ }
30270
+ function createReadFileTool() {
30271
+ return {
30272
+ name: "read_file",
30273
+ description: "Read a file from the filesystem, with optional offset and limit for line ranges. Returns content with 1-based line numbers.",
30274
+ inputSchema: {
30275
+ type: "object",
30276
+ properties: {
30277
+ path: {
30278
+ type: "string",
30279
+ description: "Path to the file to read"
30280
+ },
30281
+ offset: {
30282
+ type: "number",
30283
+ minimum: 1,
30284
+ description: "Line number to start reading from (1-based, optional)"
30285
+ },
30286
+ limit: {
30287
+ type: "number",
30288
+ minimum: 1,
30289
+ description: "Maximum number of lines to read (optional)"
30290
+ }
30291
+ },
30292
+ required: ["path"]
30293
+ },
30294
+ outputTruncation: 5e4,
30295
+ async executor(args, _env) {
30296
+ const raw = await readFile$1(args.path, "utf-8");
30297
+ const lines = raw.split("\n");
30298
+ const offset = args.offset !== void 0 ? args.offset - 1 : 0;
30299
+ const slice = args.limit !== void 0 ? lines.slice(offset, offset + args.limit) : lines.slice(offset);
30300
+ const numbered = slice.map((line, i) => {
30301
+ const lineNum = offset + i + 1;
30302
+ return `${String(lineNum).padStart(3)}\t${line}`;
30303
+ });
30304
+ return numbered.join("\n");
30305
+ }
30306
+ };
30307
+ }
30308
+ function createWriteFileTool() {
30309
+ return {
30310
+ name: "write_file",
30311
+ description: "Write content to a file, creating parent directories if needed.",
30312
+ inputSchema: {
30313
+ type: "object",
30314
+ properties: {
30315
+ path: {
30316
+ type: "string",
30317
+ description: "Path to the file to write"
30318
+ },
30319
+ content: {
30320
+ type: "string",
30321
+ description: "Content to write to the file"
30322
+ }
30323
+ },
30324
+ required: ["path", "content"]
30325
+ },
30326
+ async executor(args, _env) {
30327
+ const dir = dirname$1(args.path);
30328
+ await mkdir$1(dir, { recursive: true });
30329
+ const bytes = Buffer.byteLength(args.content, "utf-8");
30330
+ await writeFile$1(args.path, args.content, "utf-8");
30331
+ return `Wrote ${bytes} bytes to ${args.path}`;
30332
+ }
30333
+ };
30334
+ }
30335
+ function createShellTool(shellTimeoutMs) {
30336
+ return {
30337
+ name: "shell",
30338
+ description: "Execute a shell command and return its output.",
30339
+ inputSchema: {
30340
+ type: "object",
30341
+ properties: {
30342
+ command: {
30343
+ type: "string",
30344
+ description: "Shell command to execute"
30345
+ },
30346
+ timeout_ms: {
30347
+ type: "number",
30348
+ description: "Timeout in milliseconds (optional)"
30349
+ }
30350
+ },
30351
+ required: ["command"]
30352
+ },
30353
+ outputTruncation: 1e4,
30354
+ async executor(args, env) {
30355
+ const timeout = args.timeout_ms !== void 0 ? args.timeout_ms : shellTimeoutMs;
30356
+ const result = await env.exec(args.command, timeout);
30357
+ if (result.exitCode !== 0) throw new Error([result.stdout, result.stderr].filter(Boolean).join("\n") || `Command failed with exit code ${result.exitCode}`);
30358
+ const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");
30359
+ return combined || `(exit code ${result.exitCode})`;
30360
+ }
30361
+ };
30362
+ }
30363
+ function createGrepTool(shellTimeoutMs) {
30364
+ return {
30365
+ name: "grep",
30366
+ description: "Search for a regex pattern in files. Returns matching lines with filenames and line numbers.",
30367
+ inputSchema: {
30368
+ type: "object",
30369
+ properties: {
30370
+ pattern: {
30371
+ type: "string",
30372
+ description: "Regex pattern to search for"
30373
+ },
30374
+ paths: {
30375
+ type: "array",
30376
+ items: { type: "string" },
30377
+ description: "File paths or directories to search in"
30378
+ }
30379
+ },
30380
+ required: ["pattern", "paths"]
30381
+ },
30382
+ outputTruncation: 1e4,
30383
+ async executor(args, env) {
30384
+ if (/[`$\\]/.test(args.pattern) && /[`$]/.test(args.pattern)) return nodeGrepFallback(args.pattern, args.paths);
30385
+ const escapedPattern = args.pattern.replace(/"/g, "\\\"");
30386
+ const escapedPaths = args.paths.map((p) => `"${p.replace(/"/g, "\\\"")}"`).join(" ");
30387
+ const cmd = `rg --no-heading -n -- "${escapedPattern}" ${escapedPaths} 2>&1`;
30388
+ try {
30389
+ const result = await env.exec(cmd, shellTimeoutMs);
30390
+ if (result.exitCode === 0 || result.stdout.trim() !== "") return result.stdout || result.stderr;
30391
+ if (result.exitCode === 1) return result.stdout;
30392
+ throw new Error("rg not available");
30393
+ } catch (_err) {
30394
+ return nodeGrepFallback(args.pattern, args.paths);
30395
+ }
30396
+ }
30397
+ };
30398
+ }
30399
+ async function nodeGrepFallback(pattern, paths) {
30400
+ const regex$1 = new RegExp(pattern);
30401
+ const results = [];
30402
+ async function scanFile(filePath) {
30403
+ try {
30404
+ const content = await readFile$1(filePath, "utf-8");
30405
+ const lines = content.split("\n");
30406
+ for (let i = 0; i < lines.length; i++) {
30407
+ const line = lines[i] ?? "";
30408
+ if (regex$1.test(line)) results.push(`${filePath}:${i + 1}:${line}`);
30409
+ }
30410
+ } catch {}
30411
+ }
30412
+ async function scanPath(p) {
30413
+ try {
30414
+ const entries = await readdir$1(p, { withFileTypes: true });
30415
+ for (const entry of entries) {
30416
+ const full = join$1(p, entry.name);
30417
+ if (entry.isDirectory()) await scanPath(full);
30418
+ else await scanFile(full);
30419
+ }
30420
+ } catch {
30421
+ await scanFile(p);
30422
+ }
30423
+ }
30424
+ for (const p of paths) await scanPath(resolve$1(p));
30425
+ return results.join("\n");
30426
+ }
30427
+ function createGlobTool() {
30428
+ return {
30429
+ name: "glob",
30430
+ description: "Find files matching a glob pattern. Returns newline-separated matching paths.",
30431
+ inputSchema: {
30432
+ type: "object",
30433
+ properties: { pattern: {
30434
+ type: "string",
30435
+ description: "Glob pattern to match files against"
30436
+ } },
30437
+ required: ["pattern"]
30438
+ },
30439
+ outputTruncation: 5e3,
30440
+ async executor(args, env) {
30441
+ return manualGlob(args.pattern, env);
30442
+ }
30443
+ };
30444
+ }
30445
+ async function manualGlob(pattern, env) {
30446
+ try {
30447
+ const result = await env.exec(`find . -path "./${pattern}" 2>/dev/null || true`, 5e3);
30448
+ return result.stdout.trim();
30449
+ } catch {
30450
+ return "";
30451
+ }
30452
+ }
30453
+
30454
+ //#endregion
30455
+ //#region packages/factory/dist/agent/tools/anthropic-tools.js
30456
+ /**
30457
+ * Creates the edit_file tool used by Anthropic (Claude) models.
30458
+ * Performs exact string search-and-replace on file contents.
30459
+ */
30460
+ function createEditFileTool() {
30461
+ return {
30462
+ name: "edit_file",
30463
+ description: "Edit a file by replacing an exact string. The old_string must appear exactly once in the file. Provide enough context to uniquely identify the target location.",
30464
+ inputSchema: {
30465
+ type: "object",
30466
+ properties: {
30467
+ path: {
30468
+ type: "string",
30469
+ description: "Path to the file to edit"
30470
+ },
30471
+ old_string: {
30472
+ type: "string",
30473
+ description: "Exact string to search for (must be unique in the file)"
30474
+ },
30475
+ new_string: {
30476
+ type: "string",
30477
+ description: "String to replace old_string with"
30478
+ }
30479
+ },
30480
+ required: [
30481
+ "path",
30482
+ "old_string",
30483
+ "new_string"
30484
+ ]
30485
+ },
30486
+ async executor(args, _env) {
30487
+ const content = await readFile$1(args.path, "utf-8");
30488
+ let count = 0;
30489
+ let pos = 0;
30490
+ while ((pos = content.indexOf(args.old_string, pos)) !== -1) {
30491
+ count++;
30492
+ pos += args.old_string.length;
30493
+ }
30494
+ if (count === 0) throw new Error("old_string not found in file");
30495
+ if (count > 1) throw new Error(`old_string is ambiguous (found ${count} times)`);
30496
+ const updated = content.replace(args.old_string, args.new_string);
30497
+ await writeFile$1(args.path, updated, "utf-8");
30498
+ return `Edited ${args.path}`;
30499
+ }
30500
+ };
30501
+ }
30502
+
30503
+ //#endregion
30504
+ //#region packages/factory/dist/agent/tools/openai-tools.js
30505
+ /**
30506
+ * Creates the apply_patch tool used by OpenAI models.
30507
+ * Accepts v4a-format patch strings.
30508
+ */
30509
+ function createApplyPatchTool() {
30510
+ return {
30511
+ name: "apply_patch",
30512
+ description: "Apply a v4a-format patch to modify files. Format: *** Begin Patch\\n*** Update File: <path>\\n@@ <context>\\n-<removed line>\\n+<added line>\\n*** End Patch",
30513
+ inputSchema: {
30514
+ type: "object",
30515
+ properties: { patch: {
30516
+ type: "string",
30517
+ description: "v4a format patch string. Uses *** Begin Patch / *** End Patch delimiters, *** Update File: <path> or *** Add File: <path> headers, @@ hunk markers, and -/+ diff lines."
30518
+ } },
30519
+ required: ["patch"]
30520
+ },
30521
+ async executor(args, env) {
30522
+ return applyV4aPatch(args.patch, env.workdir);
30523
+ }
30524
+ };
30525
+ }
30526
+ /**
30527
+ * Applies a v4a-format patch string to the filesystem.
30528
+ * Exported as a pure helper for independent testing.
30529
+ */
30530
+ async function applyV4aPatch(patch, workdir) {
30531
+ if (!patch.includes("*** Begin Patch")) throw new Error("Malformed patch: missing *** Begin Patch delimiter");
30532
+ if (!patch.includes("*** End Patch")) throw new Error("Malformed patch: missing *** End Patch delimiter");
30533
+ const lines = patch.split("\n");
30534
+ const changedFiles = [];
30535
+ let i = 0;
30536
+ while (i < lines.length && !(lines[i] ?? "").includes("*** Begin Patch")) i++;
30537
+ i++;
30538
+ while (i < lines.length) {
30539
+ const line = lines[i] ?? "";
30540
+ if (line.includes("*** End Patch")) break;
30541
+ if (line.startsWith("*** Update File: ")) {
30542
+ const filePath = line.slice(17).trim();
30543
+ const absPath = join$1(workdir, filePath);
30544
+ i++;
30545
+ i = await applyUpdateBlock(lines, i, absPath);
30546
+ changedFiles.push(filePath);
30547
+ } else if (line.startsWith("*** Add File: ")) {
30548
+ const filePath = line.slice(14).trim();
30549
+ const absPath = join$1(workdir, filePath);
30550
+ i++;
30551
+ i = await applyAddBlock(lines, i, absPath);
30552
+ changedFiles.push(filePath);
30553
+ } else i++;
30554
+ }
30555
+ if (changedFiles.length === 0) throw new Error("Malformed patch: no file operations found");
30556
+ return `Applied patch to: ${changedFiles.join(", ")}`;
30557
+ }
30558
+ /**
30559
+ * Apply an update block (hunks with +/- lines) to an existing file.
30560
+ * Returns the next line index after the block.
30561
+ */
30562
+ async function applyUpdateBlock(lines, startIdx, absPath) {
30563
+ let content;
30564
+ try {
30565
+ content = await readFile$1(absPath, "utf-8");
30566
+ } catch {
30567
+ throw new Error(`File not found: ${absPath}`);
30568
+ }
30569
+ const contentLines = content.split("\n");
30570
+ let i = startIdx;
30571
+ while (i < lines.length) {
30572
+ const line = lines[i] ?? "";
30573
+ if (line.startsWith("*** Update File: ") || line.startsWith("*** Add File: ") || line.includes("*** End Patch")) break;
30574
+ if (line.startsWith("@@")) {
30575
+ i++;
30576
+ const removals = [];
30577
+ const additions = [];
30578
+ const context = [];
30579
+ while (i < lines.length) {
30580
+ const hunkLine = lines[i] ?? "";
30581
+ if (hunkLine.startsWith("@@") || hunkLine.startsWith("*** Update File: ") || hunkLine.startsWith("*** Add File: ") || hunkLine.includes("*** End Patch")) break;
30582
+ if (hunkLine.startsWith("-")) removals.push(hunkLine.slice(1));
30583
+ else if (hunkLine.startsWith("+")) additions.push(hunkLine.slice(1));
30584
+ else context.push(hunkLine.startsWith(" ") ? hunkLine.slice(1) : hunkLine);
30585
+ i++;
30586
+ }
30587
+ if (removals.length > 0) applyHunk(contentLines, removals, additions, context);
30588
+ else if (additions.length > 0) applyAdditionHunk(contentLines, additions, context);
30589
+ } else i++;
30590
+ }
30591
+ await writeFile$1(absPath, contentLines.join("\n"), "utf-8");
30592
+ return i;
30593
+ }
30594
+ /**
30595
+ * Apply a hunk with removals to contentLines (in-place mutation).
30596
+ */
30597
+ function applyHunk(contentLines, removals, additions, _context) {
30598
+ const firstRemoval = removals[0] ?? "";
30599
+ const startIdx = contentLines.findIndex((l) => l === firstRemoval);
30600
+ if (startIdx === -1) {
30601
+ const trimIdx = contentLines.findIndex((l) => l.trim() === firstRemoval.trim());
30602
+ if (trimIdx === -1) return;
30603
+ contentLines.splice(trimIdx, removals.length, ...additions);
30604
+ return;
30605
+ }
30606
+ contentLines.splice(startIdx, removals.length, ...additions);
30607
+ }
30608
+ /**
30609
+ * Apply a pure addition hunk (no removals) — insert after context.
30610
+ */
30611
+ function applyAdditionHunk(contentLines, additions, context) {
30612
+ if (context.length === 0) {
30613
+ contentLines.push(...additions);
30614
+ return;
30615
+ }
30616
+ const lastCtx = context[context.length - 1] ?? "";
30617
+ let idx = -1;
30618
+ for (let j$1 = contentLines.length - 1; j$1 >= 0; j$1--) if (contentLines[j$1] === lastCtx) {
30619
+ idx = j$1;
30620
+ break;
30621
+ }
30622
+ if (idx === -1) {
30623
+ contentLines.push(...additions);
30624
+ return;
30625
+ }
30626
+ contentLines.splice(idx + 1, 0, ...additions);
30627
+ }
30628
+ /**
30629
+ * Apply an add block (new file creation) from patch lines.
30630
+ * Returns the next line index after the block.
30631
+ */
30632
+ async function applyAddBlock(lines, startIdx, absPath) {
30633
+ let i = startIdx;
30634
+ const fileLines = [];
30635
+ while (i < lines.length) {
30636
+ const line = lines[i] ?? "";
30637
+ if (line.startsWith("*** Update File: ") || line.startsWith("*** Add File: ") || line.includes("*** End Patch")) break;
30638
+ if (line.startsWith("+")) fileLines.push(line.slice(1));
30639
+ i++;
30640
+ }
30641
+ await mkdir$1(dirname$1(absPath), { recursive: true });
30642
+ await writeFile$1(absPath, fileLines.join("\n"), "utf-8");
30643
+ return i;
30644
+ }
30645
+
30646
+ //#endregion
30647
+ //#region packages/factory/dist/agent/tools/gemini-tools.js
30648
+ /**
30649
+ * Creates the read_many_files tool for Gemini models.
30650
+ * Reads multiple files and returns concatenated content with headers.
30651
+ */
30652
+ function createReadManyFilesTool() {
30653
+ return {
30654
+ name: "read_many_files",
30655
+ description: "Read multiple files and return their concatenated content with file headers.",
30656
+ inputSchema: {
30657
+ type: "object",
30658
+ properties: { paths: {
30659
+ type: "array",
30660
+ items: { type: "string" },
30661
+ description: "Array of file paths to read"
30662
+ } },
30663
+ required: ["paths"]
30664
+ },
30665
+ outputTruncation: 1e5,
30666
+ async executor(args, _env) {
30667
+ const parts = [];
30668
+ for (const filePath of args.paths) try {
30669
+ const content = await readFile$1(filePath, "utf-8");
30670
+ const lines = content.split("\n");
30671
+ const numbered = lines.map((line, i) => `${String(i + 1).padStart(3)}\t${line}`).join("\n");
30672
+ parts.push(`=== ${filePath} ===\n${numbered}`);
30673
+ } catch (err) {
30674
+ const message = err instanceof Error ? err.message : String(err);
30675
+ parts.push(`=== ${filePath} ===\n(error reading file: ${message})`);
30676
+ }
30677
+ return parts.join("\n\n");
30678
+ }
30679
+ };
30680
+ }
30681
+ /**
30682
+ * Creates the list_dir tool for Gemini models.
30683
+ * Returns a directory listing with entry types (dirs first, then alpha).
30684
+ */
30685
+ function createListDirTool() {
30686
+ return {
30687
+ name: "list_dir",
30688
+ description: "List directory contents with entry types ([DIR] or [FILE]), sorted dirs first then alphabetically.",
30689
+ inputSchema: {
30690
+ type: "object",
30691
+ properties: { path: {
30692
+ type: "string",
30693
+ description: "Directory path to list"
30694
+ } },
30695
+ required: ["path"]
30696
+ },
30697
+ async executor(args, _env) {
30698
+ const entries = await readdir$1(args.path, { withFileTypes: true });
30699
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => `[DIR] ${e.name}`).sort();
30700
+ const files = entries.filter((e) => !e.isDirectory()).map((e) => `[FILE] ${e.name}`).sort();
30701
+ return [...dirs, ...files].join("\n");
30702
+ }
30703
+ };
30704
+ }
30705
+ /**
30706
+ * Creates the Gemini variant of edit_file.
30707
+ * Uses file_path (instead of path) as the parameter name to match gemini-cli conventions.
30708
+ */
30709
+ function createGeminiEditFileTool() {
30710
+ return {
30711
+ name: "edit_file",
30712
+ description: "Edit a file by replacing an exact string. The old_string must appear exactly once in the file.",
30713
+ inputSchema: {
30714
+ type: "object",
30715
+ properties: {
30716
+ file_path: {
30717
+ type: "string",
30718
+ description: "Path to the file to edit"
30719
+ },
30720
+ old_string: {
30721
+ type: "string",
30722
+ description: "Exact string to search for (must be unique in the file)"
30723
+ },
30724
+ new_string: {
30725
+ type: "string",
30726
+ description: "String to replace old_string with"
30727
+ }
30728
+ },
30729
+ required: [
30730
+ "file_path",
30731
+ "old_string",
30732
+ "new_string"
30733
+ ]
30734
+ },
30735
+ async executor(args, _env) {
30736
+ const content = await readFile$1(args.file_path, "utf-8");
30737
+ let count = 0;
30738
+ let pos = 0;
30739
+ while ((pos = content.indexOf(args.old_string, pos)) !== -1) {
30740
+ count++;
30741
+ pos += args.old_string.length;
30742
+ }
30743
+ if (count === 0) throw new Error("old_string not found in file");
30744
+ if (count > 1) throw new Error(`old_string is ambiguous (found ${count} times)`);
30745
+ const updated = content.replace(args.old_string, args.new_string);
30746
+ await mkdir$1(dirname$1(args.file_path), { recursive: true });
30747
+ await writeFile$1(args.file_path, updated, "utf-8");
30748
+ return `Edited ${args.file_path}`;
30749
+ }
30750
+ };
30751
+ }
30752
+
30753
+ //#endregion
30754
+ //#region packages/factory/dist/agent/tools/profiles.js
30755
+ /**
30756
+ * Anthropic (Claude) provider profile.
30757
+ * Uses edit_file for exact string search-and-replace.
30758
+ */
30759
+ var AnthropicProfile = class {
30760
+ model;
30761
+ id = "anthropic";
30762
+ supports_streaming = true;
30763
+ context_window_size = 2e5;
30764
+ supports_parallel_tool_calls = true;
30765
+ constructor(model) {
30766
+ this.model = model;
30767
+ }
30768
+ build_system_prompt() {
30769
+ return [
30770
+ "You are a coding agent. You have access to tools for reading and modifying files, running shell commands, and searching code.",
30771
+ "",
30772
+ "Available tools:",
30773
+ "- read_file: Read file contents with optional line range (offset/limit)",
30774
+ "- write_file: Write content to a file (creates parent dirs as needed)",
30775
+ "- edit_file: Replace an exact string in a file (old_string must be unique)",
30776
+ "- shell: Execute shell commands",
30777
+ "- grep: Search for patterns in files",
30778
+ "- glob: Find files matching a pattern",
30779
+ "",
30780
+ "Guidelines:",
30781
+ "- Always read files before editing them to understand their current content",
30782
+ "- Use edit_file for targeted changes, write_file for full rewrites",
30783
+ "- Prefer targeted edits over full file rewrites when possible"
30784
+ ].join("\n");
30785
+ }
30786
+ tools() {
30787
+ return [...createSharedTools(12e4), createEditFileTool()];
30788
+ }
30789
+ provider_options() {
30790
+ return { max_tokens: 4096 };
30791
+ }
30792
+ };
30793
+ /**
30794
+ * OpenAI provider profile.
30795
+ * Uses apply_patch for v4a-format patch application.
30796
+ * Does NOT include edit_file.
30797
+ */
30798
+ var OpenAIProfile = class {
30799
+ model;
30800
+ id = "openai";
30801
+ supports_streaming = true;
30802
+ context_window_size = 128e3;
30803
+ supports_parallel_tool_calls = true;
30804
+ constructor(model) {
30805
+ this.model = model;
30806
+ }
30807
+ build_system_prompt() {
30808
+ return [
30809
+ "You are a coding agent. You have access to tools for reading and modifying files, running shell commands, and searching code.",
30810
+ "",
30811
+ "Available tools:",
30812
+ "- read_file: Read file contents with optional line range",
30813
+ "- write_file: Write content to a file",
30814
+ "- apply_patch: Apply a v4a-format patch to modify files",
30815
+ "- shell: Execute shell commands",
30816
+ "- grep: Search for patterns in files",
30817
+ "- glob: Find files matching a pattern"
30818
+ ].join("\n");
30819
+ }
30820
+ tools() {
30821
+ return [...createSharedTools(1e4), createApplyPatchTool()];
30822
+ }
30823
+ provider_options() {
30824
+ return {};
30825
+ }
30826
+ };
30827
+ /**
30828
+ * Gemini provider profile.
30829
+ * Uses read_many_files, list_dir, and a Gemini-specific edit_file variant.
30830
+ */
30831
+ var GeminiProfile = class {
30832
+ model;
30833
+ id = "gemini";
30834
+ supports_streaming = true;
30835
+ context_window_size = 1e6;
30836
+ supports_parallel_tool_calls = true;
30837
+ constructor(model) {
30838
+ this.model = model;
30839
+ }
30840
+ build_system_prompt() {
30841
+ return [
30842
+ "You are a coding agent. You have access to tools for reading and modifying files, running shell commands, and searching code.",
30843
+ "",
30844
+ "Available tools:",
30845
+ "- read_file: Read a single file with optional line range",
30846
+ "- read_many_files: Read multiple files at once",
30847
+ "- write_file: Write content to a file",
30848
+ "- edit_file: Replace an exact string in a file",
30849
+ "- list_dir: List directory contents",
30850
+ "- shell: Execute shell commands",
30851
+ "- grep: Search for patterns in files",
30852
+ "- glob: Find files matching a pattern"
30853
+ ].join("\n");
30854
+ }
30855
+ tools() {
30856
+ return [
30857
+ ...createSharedTools(1e4),
30858
+ createReadManyFilesTool(),
30859
+ createListDirTool(),
30860
+ createGeminiEditFileTool()
30861
+ ];
30862
+ }
30863
+ provider_options() {
30864
+ return {};
30865
+ }
30866
+ };
30867
+
30868
+ //#endregion
30869
+ //#region packages/factory/dist/backend/direct-bootstrap.js
30870
+ /**
30871
+ * Bootstrap a DirectCodergenBackend for the given provider and model.
30872
+ *
30873
+ * Reads the required API key from the environment, instantiates the correct
30874
+ * provider adapter and profile, constructs an LLMClient, and returns a
30875
+ * DirectCodergenBackend configured with the given options.
30876
+ *
30877
+ * @throws Error if the required API key environment variable is missing
30878
+ * @throws Error if the provider string is not recognised
30879
+ */
30880
+ function bootstrapDirectBackend(opts) {
30881
+ const { provider, model, maxTurns, projectDir, onEvent } = opts;
30882
+ const executionEnv = {
30883
+ workdir: projectDir,
30884
+ exec: (command, timeoutMs) => {
30885
+ return new Promise((resolve$6) => {
30886
+ try {
30887
+ const stdout = execSync(command, {
30888
+ cwd: projectDir,
30889
+ timeout: timeoutMs,
30890
+ encoding: "utf-8"
30891
+ });
30892
+ resolve$6({
30893
+ stdout: stdout || "",
30894
+ stderr: "",
30895
+ exitCode: 0
30896
+ });
30897
+ } catch (err) {
30898
+ const execError = err;
30899
+ resolve$6({
30900
+ stdout: execError.stdout ? String(execError.stdout) : "",
30901
+ stderr: execError.stderr ? String(execError.stderr) : "",
30902
+ exitCode: typeof execError.status === "number" ? execError.status : 1
30903
+ });
30904
+ }
30905
+ });
30906
+ }
30907
+ };
30908
+ const client = new LLMClient();
30909
+ if (provider === "anthropic") {
30910
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
30911
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY environment variable is required for direct backend with anthropic provider");
30912
+ const adapter = new AnthropicAdapter({ apiKey });
30913
+ client.registerProvider("anthropic", adapter);
30914
+ client.registerModelPattern("claude-*", "anthropic");
30915
+ const providerProfile = new AnthropicProfile(model);
30916
+ return createDirectCodergenBackend({
30917
+ llmClient: client,
30918
+ providerProfile,
30919
+ executionEnv,
30920
+ config: { max_turns: maxTurns },
30921
+ ...onEvent !== void 0 ? { onEvent } : {}
30922
+ });
30923
+ }
30924
+ if (provider === "openai") {
30925
+ const apiKey = process.env["OPENAI_API_KEY"];
30926
+ if (!apiKey) throw new Error("OPENAI_API_KEY environment variable is required for direct backend with openai provider");
30927
+ const adapter = new OpenAIAdapter({ apiKey });
30928
+ client.registerProvider("openai", adapter);
30929
+ client.registerModelPattern("gpt-*", "openai");
30930
+ const providerProfile = new OpenAIProfile(model);
30931
+ return createDirectCodergenBackend({
30932
+ llmClient: client,
30933
+ providerProfile,
30934
+ executionEnv,
30935
+ config: { max_turns: maxTurns },
30936
+ ...onEvent !== void 0 ? { onEvent } : {}
30937
+ });
30938
+ }
30939
+ if (provider === "gemini") {
30940
+ const apiKey = process.env["GEMINI_API_KEY"];
30941
+ if (!apiKey) throw new Error("GEMINI_API_KEY environment variable is required for direct backend with gemini provider");
30942
+ const adapter = new GeminiAdapter({ apiKey });
30943
+ client.registerProvider("gemini", adapter);
30944
+ client.registerModelPattern("gemini-*", "gemini");
30945
+ const providerProfile = new GeminiProfile(model);
30946
+ return createDirectCodergenBackend({
30947
+ llmClient: client,
30948
+ providerProfile,
30949
+ executionEnv,
30950
+ config: { max_turns: maxTurns },
30951
+ ...onEvent !== void 0 ? { onEvent } : {}
30952
+ });
30953
+ }
30954
+ throw new Error(`Unknown direct backend provider: ${provider}`);
30955
+ }
30956
+
30957
+ //#endregion
30958
+ //#region packages/factory/dist/handlers/start.js
30959
+ /**
30960
+ * Handler for start nodes.
30961
+ * No side effects — edge selection drives the next step.
30962
+ *
30963
+ * Story 42-9.
30964
+ */
30965
+ /** Handler for start nodes; no side effects; edge selection drives next step. */
30966
+ const startHandler = async (_node, _context, _graph) => {
30967
+ return { status: "SUCCESS" };
30968
+ };
30969
+
30970
+ //#endregion
30971
+ //#region packages/factory/dist/handlers/exit.js
30972
+ /**
30973
+ * Handler for exit/terminal nodes.
30974
+ * Signals successful graph completion with no side effects.
30975
+ *
30976
+ * Story 42-9.
30977
+ */
30978
+ /** Handler for exit/terminal nodes; signals successful graph completion. */
30979
+ const exitHandler = async (_node, _context, _graph) => {
30980
+ return { status: "SUCCESS" };
30981
+ };
30982
+
30983
+ //#endregion
30984
+ //#region packages/factory/dist/handlers/conditional.js
30985
+ /**
30986
+ * Handler for conditional/branching nodes.
30987
+ * Routing is delegated entirely to edge selection (story 42-12).
30988
+ * This handler does nothing — it simply returns SUCCESS so the executor
30989
+ * can proceed to edge evaluation.
30990
+ *
30991
+ * Story 42-9.
30992
+ */
30993
+ /**
30994
+ * Handler for conditional/branching nodes; routing is delegated entirely to
30995
+ * edge selection (story 42-12); this handler does nothing.
30996
+ */
30997
+ const conditionalHandler = async (_node, _context, _graph) => {
30998
+ return { status: "SUCCESS" };
30999
+ };
31000
+
31001
+ //#endregion
31002
+ //#region packages/factory/dist/stylesheet/resolver.js
31003
+ /**
31004
+ * Specificity-based resolver for the model stylesheet.
31005
+ *
31006
+ * Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
31007
+ * which LLM routing properties apply to that node by:
31008
+ * 1. Filtering rules to only those whose selector matches the node.
31009
+ * 2. Sorting matching rules by specificity ascending (stable sort so that
31010
+ * equal-specificity rules preserve source order).
31011
+ * 3. Iterating the sorted list and letting each rule overwrite properties
31012
+ * — the last rule at the highest specificity wins; for ties the rule
31013
+ * appearing later in the original stylesheet wins.
31014
+ *
31015
+ * **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
31016
+ * attribute wins" rule. The caller (executor / node preparation layer) is
31017
+ * responsible for the final merge:
31018
+ * ```typescript
31019
+ * const resolved = resolveNodeStyles(node, stylesheet)
31020
+ * const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
31021
+ * ```
31022
+ *
31023
+ * Story 42-7.
31024
+ */
31025
+ /**
31026
+ * Return `true` if the given `selector` matches the provided `node`.
31027
+ *
31028
+ * Matching rules:
31029
+ * - `universal`: always matches every node.
31030
+ * - `shape`: matches when `node.shape === selector.value`.
31031
+ * - `class`: matches when the node's `class` field, split on commas and
31032
+ * trimmed, contains `selector.value` (case-sensitive).
31033
+ * - `id`: matches when `node.id === selector.value`.
31034
+ */
31035
+ function matchesNode(node, selector) {
31036
+ switch (selector.type) {
31037
+ case "universal": return true;
31038
+ case "shape": return node.shape === selector.value;
31039
+ case "class": {
31040
+ const tokens = node.class.split(",").map((t) => t.trim());
31041
+ return tokens.includes(selector.value);
31042
+ }
31043
+ case "id": return node.id === selector.value;
31044
+ default: return false;
31045
+ }
31046
+ }
31047
+ /**
31048
+ * Resolve LLM routing properties for a single graph node using the parsed stylesheet.
31049
+ *
31050
+ * @param node - The graph node to resolve properties for.
31051
+ * @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
31052
+ * @returns A `ResolvedNodeStyles` object containing only the properties that
31053
+ * were resolved from matching rules. Properties absent from matching rules
31054
+ * are omitted (not set to `undefined` explicitly).
31055
+ *
31056
+ * **Important**: this function does NOT check the node's own attributes.
31057
+ * Callers must give explicit node attributes (`node.llmModel`, etc.) priority
31058
+ * over the values returned here.
31059
+ */
31060
+ function resolveNodeStyles(node, stylesheet) {
31061
+ const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
31062
+ matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
31063
+ const resolved = {};
31064
+ for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
31065
+ else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
31066
+ else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
31067
+ return resolved;
31068
+ }
31069
+
31070
+ //#endregion
31071
+ //#region packages/factory/dist/handlers/codergen-handler.js
31072
+ const DEFAULT_MODEL = "claude-sonnet-4-5";
31073
+ const DEFAULT_PROVIDER = "anthropic";
31074
+ const DEFAULT_REASONING_EFFORT = "medium";
31075
+ /**
31076
+ * Replace all `{{key}}` placeholders in `template` with the corresponding
31077
+ * values from `context`. Missing keys resolve to empty string `""` without
31078
+ * throwing.
31079
+ *
31080
+ * @param template - The prompt template string (may contain `{{variable}}` tokens).
31081
+ * @param context - The graph context to read values from.
31082
+ * @returns The fully-interpolated prompt string.
31083
+ */
31084
+ function interpolatePrompt(template, context) {
31085
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
31086
+ return context.getString(key, "");
31087
+ });
31088
+ }
31089
+ /**
31090
+ * Resolve the LLM routing properties for a graph node.
31091
+ *
31092
+ * Priority (highest wins):
31093
+ * 1. Node-level attributes (`node.llmModel`, `node.llmProvider`, `node.reasoningEffort`)
31094
+ * 2. Stylesheet-resolved values via `resolveNodeStyles`
31095
+ * 3. Per-option defaults (`options.defaultModel`, etc.)
31096
+ * 4. System defaults (`claude-sonnet-4-5`, `anthropic`, `medium`)
31097
+ */
31098
+ function resolveModel(node, stylesheet, options) {
31099
+ const stylesheetResolved = stylesheet ? resolveNodeStyles(node, stylesheet) : {};
31100
+ const llm_model = node.llmModel || stylesheetResolved.llmModel || options?.defaultModel || DEFAULT_MODEL;
31101
+ const llm_provider = node.llmProvider || stylesheetResolved.llmProvider || options?.defaultProvider || DEFAULT_PROVIDER;
31102
+ const reasoning_effort = node.reasoningEffort || stylesheetResolved.reasoningEffort || options?.defaultReasoningEffort || DEFAULT_REASONING_EFFORT;
31103
+ return {
31104
+ llm_model,
31105
+ llm_provider,
31106
+ reasoning_effort
31107
+ };
31108
+ }
31109
+ /**
31110
+ * Returns `true` for errors that are transient and should trigger a
31111
+ * `NEEDS_RETRY` outcome:
31112
+ * - HTTP status 429 (rate limit)
31113
+ * - Errors whose message contains `"timeout"`, `"ETIMEDOUT"`,
31114
+ * `"ECONNRESET"`, or `"ECONNREFUSED"`
31115
+ *
31116
+ * All other errors are non-transient and map to `FAILURE`.
31117
+ */
31118
+ function isTransientError(error) {
31119
+ if (error == null) return false;
31120
+ const status = error.status ?? error.statusCode;
31121
+ if (status === 429) return true;
31122
+ const message = error.message;
31123
+ if (typeof message === "string") {
31124
+ if (message.includes("timeout") || message.includes("ETIMEDOUT") || message.includes("ECONNRESET") || message.includes("ECONNREFUSED")) return true;
31125
+ }
31126
+ return false;
31127
+ }
31128
+ /**
31129
+ * Create a codergen node handler configured with the given options.
31130
+ *
31131
+ * @param options - Optional configuration (stylesheet, default model/provider/effort).
31132
+ * @returns A `NodeHandler` that:
31133
+ * 1. Interpolates the node's prompt (or label) against GraphContext.
31134
+ * 2. Resolves LLM routing properties.
31135
+ * 3. Calls the LLM client from `\@substrate-ai/core`.
31136
+ * 4. Returns a `SUCCESS` outcome with the response in `contextUpdates`,
31137
+ * `NEEDS_RETRY` for transient errors, or `FAILURE` for others.
31138
+ */
31139
+ function createCodergenHandler(options) {
31140
+ return async (node, context, _graph) => {
31141
+ const template = node.prompt || node.label || "";
31142
+ const interpolatedPrompt = interpolatePrompt(template, context);
31143
+ if (options?.backend) return options.backend.run(node, interpolatedPrompt, context);
31144
+ if (node.backend === "direct" && options?.directBackend) return options.directBackend.run(node, interpolatedPrompt, context);
31145
+ const { llm_model, llm_provider, reasoning_effort } = resolveModel(node, options?.stylesheet, options);
31146
+ try {
31147
+ const result = await callLLM({
31148
+ model: llm_model,
31149
+ provider: llm_provider,
31150
+ reasoningEffort: reasoning_effort,
31151
+ prompt: interpolatedPrompt
31152
+ });
31153
+ const responseText = result.text;
31154
+ return {
31155
+ status: "SUCCESS",
31156
+ notes: responseText,
31157
+ contextUpdates: { [`${node.id}_output`]: responseText }
31158
+ };
31159
+ } catch (error) {
31160
+ if (isTransientError(error)) return {
31161
+ status: "NEEDS_RETRY",
31162
+ error
31163
+ };
31164
+ return {
31165
+ status: "FAILURE",
31166
+ error
31167
+ };
31168
+ }
31169
+ };
31170
+ }
31171
+
31172
+ //#endregion
31173
+ //#region packages/factory/dist/handlers/tool.js
31174
+ /**
31175
+ * Duck-typing guard: returns true if `parsed` conforms to the ScenarioRunResult
31176
+ * shape (has summary.total and summary.passed as numbers).
31177
+ *
28962
31178
  * Detection is intentionally command-agnostic — it operates on the JSON shape
28963
31179
  * alone, not on the value of `node.toolCommand`.
28964
31180
  */
@@ -29176,19 +31392,19 @@ var HandlerRegistry = class {
29176
31392
  * `shape=box` nodes, and as the registry-level default for any node that has no
29177
31393
  * recognised type or shape mapping.
29178
31394
  */
29179
- function createDefaultRegistry() {
31395
+ function createDefaultRegistry(options) {
29180
31396
  const registry = new HandlerRegistry();
29181
31397
  registry.register("start", startHandler);
29182
31398
  registry.register("exit", exitHandler);
29183
31399
  registry.register("conditional", conditionalHandler);
29184
- registry.register("codergen", createCodergenHandler());
31400
+ registry.register("codergen", createCodergenHandler(options));
29185
31401
  registry.register("tool", createToolHandler());
29186
31402
  registry.register("wait.human", createWaitHumanHandler());
29187
31403
  registry.registerShape("Mdiamond", "start");
29188
31404
  registry.registerShape("Msquare", "exit");
29189
31405
  registry.registerShape("diamond", "conditional");
29190
31406
  registry.registerShape("box", "codergen");
29191
- registry.setDefault(createCodergenHandler());
31407
+ registry.setDefault(createCodergenHandler(options));
29192
31408
  return registry;
29193
31409
  }
29194
31410
 
@@ -34491,7 +36707,20 @@ const FactoryConfigSchema = z.object({
34491
36707
  "dual-signal",
34492
36708
  "scenario-primary",
34493
36709
  "scenario-only"
34494
- ]).default("dual-signal")
36710
+ ]).default("dual-signal"),
36711
+ direct_backend: z.object({
36712
+ provider: z.enum([
36713
+ "anthropic",
36714
+ "openai",
36715
+ "gemini"
36716
+ ]).default("anthropic"),
36717
+ model: z.string().default("claude-3-5-sonnet-20241022"),
36718
+ max_turns: z.number().int().min(1).default(20)
36719
+ }).default({
36720
+ provider: "anthropic",
36721
+ model: "claude-3-5-sonnet-20241022",
36722
+ max_turns: 20
36723
+ })
34495
36724
  }).strict();
34496
36725
  /**
34497
36726
  * Extends SubstrateConfigSchema with an optional `factory:` section.
@@ -34673,7 +36902,7 @@ const TOTAL_RULE_COUNT = 13;
34673
36902
  function registerFactoryCommand(program, options) {
34674
36903
  const factoryCmd = program.command("factory").description("Factory pipeline and scenario management commands");
34675
36904
  registerScenariosCommand(factoryCmd);
34676
- factoryCmd.command("run").description("Execute a DOT graph pipeline").option("--graph <path>", "Path to DOT graph file").option("--config <path>", "Path to config.yaml (default: auto-detect)").option("--events", "Emit NDJSON events to stdout").action(async (opts) => {
36905
+ factoryCmd.command("run").description("Execute a DOT graph pipeline").option("--graph <path>", "Path to DOT graph file").option("--config <path>", "Path to config.yaml (default: auto-detect)").option("--events", "Emit NDJSON events to stdout").option("--backend <mode>", "Backend: cli | direct (overrides config factory.backend)").action(async (opts) => {
34677
36906
  try {
34678
36907
  const projectDir = process.cwd();
34679
36908
  const graphPath = await resolveGraphPath(opts, projectDir);
@@ -34709,6 +36938,18 @@ function registerFactoryCommand(program, options) {
34709
36938
  type: "factory:config-reloaded",
34710
36939
  ...e
34711
36940
  }));
36941
+ eventBus.on("agent:tool-call", (e) => emit({
36942
+ type: "agent:tool-call",
36943
+ ...e
36944
+ }));
36945
+ eventBus.on("agent:loop-detected", (e) => emit({
36946
+ type: "agent:loop-detected",
36947
+ ...e
36948
+ }));
36949
+ eventBus.on("agent:steering-injected", (e) => emit({
36950
+ type: "agent:steering-injected",
36951
+ ...e
36952
+ }));
34712
36953
  }
34713
36954
  process.stdout.write(`Running graph pipeline from ${graphPath}\n`);
34714
36955
  const runId = randomUUID();
@@ -34717,6 +36958,63 @@ function registerFactoryCommand(program, options) {
34717
36958
  await stateManager.initRun(dotSource);
34718
36959
  /** wallClockCapMs: FactoryConfig.wall_clock_cap_seconds × 1000 (story 45-10) */
34719
36960
  const factoryConfig = await loadFactoryConfig(projectDir, opts.config);
36961
+ const effectiveBackend = opts.backend ?? factoryConfig.factory?.backend ?? "cli";
36962
+ let directBackend;
36963
+ if (effectiveBackend === "direct") {
36964
+ let currentNodeId = "";
36965
+ eventBus.on("graph:node-started", (e) => {
36966
+ currentNodeId = e.nodeId;
36967
+ });
36968
+ const toolCallNameMap = new Map();
36969
+ const onDirectEvent = (event) => {
36970
+ if (event.kind === EventKind.TOOL_CALL_START) {
36971
+ const toolName = event.data["tool_name"] ?? "";
36972
+ const callId = event.data["call_id"] ?? "";
36973
+ toolCallNameMap.set(callId, toolName);
36974
+ eventBus.emit("agent:tool-call", {
36975
+ runId,
36976
+ nodeId: currentNodeId,
36977
+ toolName,
36978
+ direction: "call"
36979
+ });
36980
+ } else if (event.kind === EventKind.TOOL_CALL_END) {
36981
+ const callId = event.data["call_id"] ?? "";
36982
+ const toolName = toolCallNameMap.get(callId) ?? "";
36983
+ toolCallNameMap.delete(callId);
36984
+ eventBus.emit("agent:tool-call", {
36985
+ runId,
36986
+ nodeId: currentNodeId,
36987
+ toolName,
36988
+ direction: "result"
36989
+ });
36990
+ } else if (event.kind === EventKind.LOOP_DETECTION) eventBus.emit("agent:loop-detected", {
36991
+ runId,
36992
+ nodeId: currentNodeId,
36993
+ windowSize: event.data["windowSize"] ?? 0,
36994
+ pattern: event.data["pattern"] ?? []
36995
+ });
36996
+ else if (event.kind === EventKind.STEERING_INJECTED) eventBus.emit("agent:steering-injected", {
36997
+ runId,
36998
+ nodeId: currentNodeId,
36999
+ message: event.data["content"] ?? ""
37000
+ });
37001
+ };
37002
+ try {
37003
+ const directBackendCfg = factoryConfig.factory?.direct_backend;
37004
+ directBackend = bootstrapDirectBackend({
37005
+ provider: directBackendCfg?.provider ?? "anthropic",
37006
+ model: directBackendCfg?.model ?? "claude-3-5-sonnet-20241022",
37007
+ maxTurns: directBackendCfg?.max_turns ?? 20,
37008
+ projectDir,
37009
+ onEvent: onDirectEvent
37010
+ });
37011
+ } catch (err) {
37012
+ const msg = err instanceof Error ? err.message : String(err);
37013
+ process.stderr.write(`Error: ${msg}\n`);
37014
+ process.exit(1);
37015
+ return;
37016
+ }
37017
+ }
34720
37018
  const adapter = options?.createAdapter ? options.createAdapter(projectDir) : createDatabaseAdapter$1({
34721
37019
  backend: "auto",
34722
37020
  basePath: projectDir
@@ -34726,7 +37024,7 @@ function registerFactoryCommand(program, options) {
34726
37024
  const executorConfig = {
34727
37025
  runId,
34728
37026
  logsRoot,
34729
- handlerRegistry: createDefaultRegistry(),
37027
+ handlerRegistry: createDefaultRegistry(directBackend ? { directBackend } : void 0),
34730
37028
  eventBus,
34731
37029
  dotSource,
34732
37030
  adapter,
@@ -36454,4 +38752,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
36454
38752
 
36455
38753
  //#endregion
36456
38754
  export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
36457
- //# sourceMappingURL=run-DZwHUMeE.js.map
38755
+ //# sourceMappingURL=run-Bku8fmY3.js.map