u-foo 1.9.7 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/bin/ufoo.js +5 -3
  2. package/package.json +2 -4
  3. package/src/agent/claudeEventTranslator.js +267 -0
  4. package/src/agent/claudeOauthTokenReader.js +52 -0
  5. package/src/agent/claudeThreadProvider.js +343 -0
  6. package/src/agent/cliRunner.js +10 -16
  7. package/src/agent/codexEventTranslator.js +78 -0
  8. package/src/agent/codexThreadProvider.js +181 -0
  9. package/src/agent/controllerToolExecutor.js +233 -0
  10. package/src/agent/credentials/claude.js +324 -0
  11. package/src/agent/credentials/codex.js +203 -0
  12. package/src/agent/credentials/index.js +106 -0
  13. package/src/agent/internalRunner.js +348 -3
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ptyRunner.js +8 -7
  17. package/src/agent/ufooAgent.js +178 -120
  18. package/src/agent/upstreamTransport.js +464 -0
  19. package/src/bus/utils.js +3 -2
  20. package/src/chat/dashboardView.js +51 -1
  21. package/src/chat/index.js +3 -1
  22. package/src/config.js +53 -17
  23. package/src/controller/flags.js +160 -0
  24. package/src/controller/gateRouter.js +201 -0
  25. package/src/controller/routerFastPath.js +22 -0
  26. package/src/controller/shadowGuard.js +280 -0
  27. package/src/daemon/index.js +2 -3
  28. package/src/daemon/promptLoop.js +33 -224
  29. package/src/daemon/promptRequest.js +360 -5
  30. package/src/daemon/status.js +2 -0
  31. package/src/history/inputTimeline.js +9 -4
  32. package/src/memory/index.js +24 -0
  33. package/src/providerapi/redactor.js +87 -0
  34. package/src/providerapi/shadowDiff.js +174 -0
  35. package/src/report/store.js +4 -3
  36. package/src/tools/handlers/ackBus.js +26 -0
  37. package/src/tools/handlers/common.js +64 -0
  38. package/src/tools/handlers/dispatchMessage.js +81 -0
  39. package/src/tools/handlers/listAgents.js +14 -0
  40. package/src/tools/handlers/readBusSummary.js +34 -0
  41. package/src/tools/handlers/readOpenDecisions.js +26 -0
  42. package/src/tools/handlers/readProjectRegistry.js +20 -0
  43. package/src/tools/handlers/readPromptHistory.js +123 -0
  44. package/src/tools/handlers/tier2.js +134 -0
  45. package/src/tools/index.js +55 -0
  46. package/src/tools/registry.js +69 -0
  47. package/src/tools/schemaFixtures.js +415 -0
  48. package/src/tools/tier0/listAgents.js +14 -0
  49. package/src/tools/tier0/readBusSummary.js +14 -0
  50. package/src/tools/tier0/readOpenDecisions.js +14 -0
  51. package/src/tools/tier0/readProjectRegistry.js +14 -0
  52. package/src/tools/tier0/readPromptHistory.js +14 -0
  53. package/src/tools/tier1/ackBus.js +14 -0
  54. package/src/tools/tier1/dispatchMessage.js +14 -0
  55. package/src/tools/tier1/routeAgent.js +14 -0
  56. package/src/tools/tier2/closeAgent.js +14 -0
  57. package/src/tools/tier2/launchAgent.js +14 -0
  58. package/src/tools/tier2/manageCron.js +14 -0
  59. package/src/tools/tier2/renameAgent.js +14 -0
  60. package/src/tools/types.js +75 -0
  61. package/src/tools/unimplemented.js +13 -0
  62. package/src/ufoo/paths.js +4 -0
  63. package/bin/ufoo-assistant-agent.js +0 -5
  64. package/bin/ufoo-engine.js +0 -25
  65. package/src/assistant/agent.js +0 -261
  66. package/src/assistant/bridge.js +0 -178
  67. package/src/assistant/constants.js +0 -15
  68. package/src/assistant/engine.js +0 -252
  69. package/src/assistant/stdio.js +0 -58
  70. package/src/assistant/ufooEngineCli.js +0 -312
@@ -1,24 +1,12 @@
1
1
  const { spawn } = require("child_process");
2
2
  const { randomUUID } = require("crypto");
3
- const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
3
+
4
+ const DEFAULT_CLI_TIMEOUT_MS = 600000;
4
5
 
5
6
  const ROUTER_JSON_SCHEMA = JSON.stringify({
6
7
  type: "object",
7
8
  properties: {
8
9
  reply: { type: "string" },
9
- assistant_call: {
10
- type: "object",
11
- properties: {
12
- kind: { type: "string", enum: ["explore", "bash", "mixed"] },
13
- task: { type: "string" },
14
- context: { type: "string" },
15
- expect: { type: "string" },
16
- provider: { type: "string" },
17
- model: { type: "string" },
18
- timeout_ms: { type: "integer" },
19
- },
20
- required: ["task"],
21
- },
22
10
  dispatch: {
23
11
  type: "array",
24
12
  items: {
@@ -484,6 +472,10 @@ const DEFAULT_CODEX = {
484
472
 
485
473
  function buildArgs(backend, prompt, opts) {
486
474
  const args = [...(backend.args || [])];
475
+ const extraArgs = Array.isArray(opts.extraArgs) ? opts.extraArgs.filter(Boolean) : [];
476
+ if (extraArgs.length > 0) {
477
+ args.push(...extraArgs);
478
+ }
487
479
  if (opts.model && backend.modelArg) {
488
480
  args.push(backend.modelArg, opts.model);
489
481
  }
@@ -588,6 +580,7 @@ async function runCliAgent(params) {
588
580
  sessionId,
589
581
  systemPrompt: params.systemPrompt,
590
582
  disableSession: params.disableSession,
583
+ extraArgs: params.extraArgs,
591
584
  });
592
585
  if (backend === DEFAULT_CODEX && params.sandbox) {
593
586
  applySandboxOverride(args, params.sandbox);
@@ -617,7 +610,7 @@ async function runCliAgent(params) {
617
610
  cwd: params.cwd,
618
611
  env,
619
612
  input: stdin,
620
- timeoutMs: params.timeoutMs || DEFAULT_ASSISTANT_TIMEOUT_MS,
613
+ timeoutMs: params.timeoutMs || DEFAULT_CLI_TIMEOUT_MS,
621
614
  onStdout: codexParser ? (chunk) => codexParser.onChunk(chunk) : null,
622
615
  signal: params.signal,
623
616
  });
@@ -642,6 +635,7 @@ async function runCliAgent(params) {
642
635
  sessionId,
643
636
  systemPrompt: params.systemPrompt,
644
637
  disableSession: params.disableSession,
638
+ extraArgs: params.extraArgs,
645
639
  },
646
640
  );
647
641
  retryArgs = retry.args;
@@ -679,7 +673,7 @@ async function runCliAgent(params) {
679
673
  cwd: params.cwd,
680
674
  env,
681
675
  input: retryStdin,
682
- timeoutMs: params.timeoutMs || DEFAULT_ASSISTANT_TIMEOUT_MS,
676
+ timeoutMs: params.timeoutMs || DEFAULT_CLI_TIMEOUT_MS,
683
677
  onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
684
678
  signal: params.signal,
685
679
  });
@@ -0,0 +1,78 @@
1
+ function getTextFromItem(item) {
2
+ if (!item || typeof item !== "object") return "";
3
+ if (typeof item.text === "string") return item.text;
4
+ if (item.item && typeof item.item.text === "string") return item.item.text;
5
+ if (Array.isArray(item.content)) {
6
+ return item.content
7
+ .map((entry) => (entry && typeof entry.text === "string" ? entry.text : ""))
8
+ .join("");
9
+ }
10
+ return "";
11
+ }
12
+
13
+ function normalizeCodexEvent(event = {}) {
14
+ const type = String(event.type || "").trim();
15
+ if (!type) return null;
16
+
17
+ if (type === "thread.started") {
18
+ return { type: "thread_started", threadId: event.thread_id || event.threadId || "" };
19
+ }
20
+
21
+ if (type === "turn.started") {
22
+ return { type: "turn_started", turnId: event.turn_id || event.turnId || "" };
23
+ }
24
+
25
+ if (type === "turn.completed") {
26
+ return {
27
+ type: "turn_completed",
28
+ turnId: event.turn_id || event.turnId || "",
29
+ usage: event.usage || null,
30
+ };
31
+ }
32
+
33
+ if (type === "turn.failed") {
34
+ const error = event.error || {};
35
+ return {
36
+ type: "turn_failed",
37
+ turnId: event.turn_id || event.turnId || "",
38
+ error: typeof error.message === "string" ? error.message : String(error || "turn failed"),
39
+ };
40
+ }
41
+
42
+ if (type === "item.completed") {
43
+ const item = event.item || {};
44
+ const itemType = String(item.type || "").trim();
45
+ const text = getTextFromItem(item);
46
+
47
+ if (itemType === "message" || itemType === "assistant_message" || text) {
48
+ return {
49
+ type: "text_delta",
50
+ delta: text,
51
+ itemType,
52
+ };
53
+ }
54
+
55
+ if (itemType === "tool_call") {
56
+ return {
57
+ type: "tool_call",
58
+ name: item.name || "",
59
+ toolCallId: item.id || item.tool_call_id || "",
60
+ args: item.arguments || item.args || {},
61
+ };
62
+ }
63
+
64
+ if (itemType === "tool_result") {
65
+ return {
66
+ type: "tool_result",
67
+ toolCallId: item.tool_call_id || item.id || "",
68
+ output: item.output,
69
+ };
70
+ }
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ module.exports = {
77
+ normalizeCodexEvent,
78
+ };
@@ -0,0 +1,181 @@
1
+ const { normalizeCodexEvent } = require("./codexEventTranslator");
2
+ const { redactUfooEvent } = require("../providerapi/redactor");
3
+ const { sendUpstreamPrompt } = require("./upstreamTransport");
4
+
5
+ function resolveCodexSdk() {
6
+ try {
7
+ // Optional dependency during Phase 1a seam work.
8
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
9
+ return require("@openai/codex-sdk");
10
+ } catch (err) {
11
+ const error = new Error("Codex SDK seam enabled but @openai/codex-sdk is not installed");
12
+ error.code = "CODEX_SDK_UNAVAILABLE";
13
+ error.cause = err;
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ function defaultCodexStreamFactory({
19
+ sdk,
20
+ model,
21
+ cwd,
22
+ extraArgs = [],
23
+ threadId = "",
24
+ input,
25
+ opts = {},
26
+ }) {
27
+ if (!sdk || typeof sdk.runStreamed !== "function") {
28
+ throw new Error("Codex SDK seam requires runStreamed support");
29
+ }
30
+ const { history, ...sdkOpts } = opts;
31
+ void history;
32
+ return sdk.runStreamed({
33
+ model,
34
+ cwd,
35
+ extraArgs,
36
+ threadId,
37
+ input,
38
+ ...sdkOpts,
39
+ });
40
+ }
41
+
42
+ function createThreadId() {
43
+ return `codex-thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
44
+ }
45
+
46
+ async function* defaultCodexTransportStreamFactory({
47
+ model,
48
+ cwd,
49
+ threadId = "",
50
+ input,
51
+ opts = {},
52
+ }) {
53
+ const nextThreadId = String(threadId || "").trim() || createThreadId();
54
+ const result = await sendUpstreamPrompt({
55
+ projectRoot: cwd,
56
+ provider: "codex",
57
+ model,
58
+ prompt: String(input || ""),
59
+ messages: Array.isArray(opts.history) ? opts.history : [],
60
+ timeoutMs: Number.isFinite(Number(opts.timeoutMs)) ? Number(opts.timeoutMs) : 120000,
61
+ });
62
+ if (!result.ok) {
63
+ const err = new Error(result.error || "Codex upstream request failed");
64
+ err.code = "CODEX_UPSTREAM_FAILED";
65
+ throw err;
66
+ }
67
+
68
+ yield { type: "thread.started", thread_id: nextThreadId };
69
+ yield {
70
+ type: "item.completed",
71
+ item: { type: "message", text: String(result.output || "") },
72
+ };
73
+ yield {
74
+ type: "turn.completed",
75
+ turn_id: `turn-${Date.now().toString(36)}`,
76
+ usage: result.usage || null,
77
+ };
78
+ }
79
+
80
+ class CodexSdkThread {
81
+ constructor({
82
+ model = "",
83
+ cwd = "",
84
+ extraArgs = [],
85
+ streamFactory = defaultCodexStreamFactory,
86
+ sdk,
87
+ tools = [],
88
+ } = {}) {
89
+ this.id = "";
90
+ this.model = model;
91
+ this.cwd = cwd;
92
+ this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
93
+ this.streamFactory = streamFactory;
94
+ this.sdk = sdk;
95
+ this.tools = Array.isArray(tools) ? tools.slice() : [];
96
+ this.messages = [];
97
+ }
98
+
99
+ async *runStreamed(input, opts = {}) {
100
+ const mergedOpts = { ...opts };
101
+ if (this.tools.length > 0 && !Array.isArray(mergedOpts.tools)) {
102
+ mergedOpts.tools = this.tools.slice();
103
+ }
104
+ mergedOpts.history = this.messages.slice();
105
+ const stream = this.streamFactory({
106
+ sdk: this.sdk,
107
+ model: this.model,
108
+ cwd: this.cwd,
109
+ extraArgs: this.extraArgs,
110
+ threadId: this.id,
111
+ input,
112
+ opts: mergedOpts,
113
+ });
114
+ let outputText = "";
115
+ for await (const rawEvent of stream) {
116
+ const normalized = normalizeCodexEvent(rawEvent);
117
+ if (!normalized) continue;
118
+ if (normalized.type === "thread_started" && normalized.threadId) {
119
+ this.id = normalized.threadId;
120
+ }
121
+ if (normalized.type === "text_delta" && normalized.delta) {
122
+ outputText += normalized.delta;
123
+ }
124
+ yield redactUfooEvent(normalized);
125
+ }
126
+ this.messages.push({ role: "user", content: String(input || "") });
127
+ this.messages.push({ role: "assistant", content: outputText });
128
+ }
129
+
130
+ async close() {
131
+ return undefined;
132
+ }
133
+ }
134
+
135
+ class CodexThreadProvider {
136
+ constructor({
137
+ model = "",
138
+ cwd = "",
139
+ extraArgs = [],
140
+ streamFactory = defaultCodexStreamFactory,
141
+ sdk,
142
+ tools = [],
143
+ } = {}) {
144
+ this.model = model;
145
+ this.cwd = cwd;
146
+ this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
+ this.streamFactory = streamFactory;
148
+ this.sdk = sdk || resolveCodexSdk();
149
+ this.tools = Array.isArray(tools) ? tools.slice() : [];
150
+ }
151
+
152
+ startThread() {
153
+ return new CodexSdkThread({
154
+ model: this.model,
155
+ cwd: this.cwd,
156
+ extraArgs: this.extraArgs,
157
+ streamFactory: this.streamFactory,
158
+ sdk: this.sdk,
159
+ tools: this.tools,
160
+ });
161
+ }
162
+
163
+ resumeThread(threadId = "") {
164
+ const thread = this.startThread();
165
+ thread.id = String(threadId || "").trim();
166
+ return thread;
167
+ }
168
+ }
169
+
170
+ function createCodexThreadProvider(options = {}) {
171
+ return new CodexThreadProvider(options);
172
+ }
173
+
174
+ module.exports = {
175
+ CodexSdkThread,
176
+ CodexThreadProvider,
177
+ createCodexThreadProvider,
178
+ defaultCodexStreamFactory,
179
+ defaultCodexTransportStreamFactory,
180
+ resolveCodexSdk,
181
+ };
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+
3
+ const EventBus = require("../bus");
4
+ const { dispatchMessageHandler } = require("../tools/handlers/dispatchMessage");
5
+ const { ackBusHandler } = require("../tools/handlers/ackBus");
6
+
7
+ function normalizeObjectArgs(value) {
8
+ if (!value) return {};
9
+ if (typeof value === "string") {
10
+ try {
11
+ const parsed = JSON.parse(value);
12
+ return parsed && typeof parsed === "object" ? parsed : {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+ return typeof value === "object" ? value : {};
18
+ }
19
+
20
+ function normalizePositiveInt(value, fallback) {
21
+ const num = Number.parseInt(value, 10);
22
+ if (Number.isFinite(num) && num > 0) return num;
23
+ return fallback;
24
+ }
25
+
26
+ function buildStructuredError(code, message, extra = {}) {
27
+ return {
28
+ code: String(code || "tool_error"),
29
+ message: String(message || "tool execution failed"),
30
+ ...extra,
31
+ };
32
+ }
33
+
34
+ function resolveDispatchTargets(ctx, target) {
35
+ if (typeof ctx.resolveDispatchTargets === "function") {
36
+ const resolved = ctx.resolveDispatchTargets(target);
37
+ if (Array.isArray(resolved) && resolved.length > 0) return resolved;
38
+ }
39
+
40
+ const eventBus = ctx.eventBus || new EventBus(ctx.projectRoot);
41
+ try {
42
+ if (typeof eventBus.ensureBus === "function") eventBus.ensureBus();
43
+ if (typeof eventBus.loadBusData === "function") eventBus.loadBusData();
44
+ const targets = eventBus.messageManager && typeof eventBus.messageManager.resolveTarget === "function"
45
+ ? eventBus.messageManager.resolveTarget(target)
46
+ : [];
47
+ if (Array.isArray(targets) && targets.length > 0) return targets;
48
+ } catch {
49
+ // fall through to conservative fallback
50
+ }
51
+
52
+ if (target.includes(":")) return [target];
53
+
54
+ throw buildStructuredError("invalid_target", `dispatch_message target not found: ${target}`);
55
+ }
56
+
57
+ async function handleDispatchMessage(ctx, args) {
58
+ const eventBus = typeof ctx.dispatchMessages === "function"
59
+ ? {
60
+ send: async (target, message, publisher, options = {}) => {
61
+ resolveDispatchTargets(ctx, target);
62
+ if (target !== "broadcast" && typeof ctx.markPending === "function") {
63
+ ctx.markPending(target);
64
+ }
65
+ await ctx.dispatchMessages(ctx.projectRoot, [{
66
+ target,
67
+ message,
68
+ injection_mode: options.injectionMode || "immediate",
69
+ source: options.source || publisher,
70
+ }]);
71
+ return { seq: 0, targets: [target] };
72
+ },
73
+ broadcast: async (message, publisher, options = {}) => {
74
+ await ctx.dispatchMessages(ctx.projectRoot, [{
75
+ target: "broadcast",
76
+ message,
77
+ injection_mode: options.injectionMode || "immediate",
78
+ source: options.source || publisher,
79
+ }]);
80
+ return { seq: 0, targets: ["broadcast"] };
81
+ },
82
+ }
83
+ : new EventBus(ctx.projectRoot);
84
+
85
+ const result = await dispatchMessageHandler({
86
+ projectRoot: ctx.projectRoot,
87
+ subscriber: ctx.subscriber,
88
+ eventBus,
89
+ }, args);
90
+
91
+ return {
92
+ dispatched: 1,
93
+ target: result.target,
94
+ injection_mode: result.mode,
95
+ source: result.source,
96
+ };
97
+ }
98
+
99
+ async function handleAckBus(ctx, args) {
100
+ const eventBus = typeof ctx.ackBus === "function"
101
+ ? {
102
+ ack: async (subscriber) => {
103
+ await ctx.ackBus(ctx.projectRoot, subscriber);
104
+ return 1;
105
+ },
106
+ }
107
+ : new EventBus(ctx.projectRoot);
108
+
109
+ const result = await ackBusHandler({
110
+ projectRoot: ctx.projectRoot,
111
+ subscriber: ctx.subscriber,
112
+ eventBus,
113
+ }, args);
114
+
115
+ return {
116
+ acknowledged: true,
117
+ subscriber: result.subscriber,
118
+ };
119
+ }
120
+
121
+ async function handleLaunchAgent(ctx, args) {
122
+ if (typeof ctx.handleOps !== "function") {
123
+ throw buildStructuredError("tool_unavailable", "launch_agent hook is unavailable");
124
+ }
125
+
126
+ const agent = String(args.agent || "").trim().toLowerCase();
127
+ if (!agent) {
128
+ throw buildStructuredError("invalid_arguments", "launch_agent requires agent");
129
+ }
130
+
131
+ const op = {
132
+ action: "launch",
133
+ agent,
134
+ count: normalizePositiveInt(args.count, 1),
135
+ };
136
+ if (args.nickname) op.nickname = String(args.nickname).trim();
137
+ if (args.prompt_profile) op.prompt_profile = String(args.prompt_profile).trim();
138
+ if (args.launch_scope) op.launch_scope = String(args.launch_scope).trim();
139
+ if (args.terminal_app) op.terminal_app = String(args.terminal_app).trim();
140
+
141
+ const opsResults = await ctx.handleOps(ctx.projectRoot, [op], ctx.processManager || null);
142
+ return {
143
+ operation: op,
144
+ ops_results: Array.isArray(opsResults) ? opsResults : [],
145
+ };
146
+ }
147
+
148
+ async function executeControllerTool(ctx, toolCall = {}) {
149
+ const observer = ctx.observer || { emit: () => {}, audit: () => {} };
150
+ const name = String(toolCall.name || "").trim();
151
+ const args = normalizeObjectArgs(toolCall.arguments);
152
+ const toolCallId = String(
153
+ toolCall.tool_call_id || toolCall.toolCallId || toolCall.id || `${Date.now().toString(36)}`
154
+ ).trim();
155
+ const turnId = String(toolCall.turn_id || toolCall.turnId || ctx.turnId || "").trim();
156
+
157
+ observer.emit("tool_call_started", {
158
+ tool_name: name,
159
+ tool_call_id: toolCallId,
160
+ turn_id: turnId,
161
+ });
162
+
163
+ try {
164
+ let result;
165
+ if (name === "dispatch_message") {
166
+ result = await handleDispatchMessage(ctx, args);
167
+ } else if (name === "ack_bus") {
168
+ result = await handleAckBus(ctx, args);
169
+ } else if (name === "launch_agent") {
170
+ result = await handleLaunchAgent(ctx, args);
171
+ } else {
172
+ throw buildStructuredError("unsupported_tool", `unsupported controller tool: ${name}`);
173
+ }
174
+
175
+ observer.emit("tool_call_finished", {
176
+ tool_name: name,
177
+ tool_call_id: toolCallId,
178
+ turn_id: turnId,
179
+ ok: true,
180
+ });
181
+ observer.audit({
182
+ source: "controller_loop",
183
+ kind: "tool_call",
184
+ tool_name: name,
185
+ tool_call_id: toolCallId,
186
+ turn_id: turnId,
187
+ ok: true,
188
+ result,
189
+ });
190
+
191
+ return {
192
+ ok: true,
193
+ name,
194
+ tool_call_id: toolCallId,
195
+ turn_id: turnId,
196
+ result,
197
+ };
198
+ } catch (err) {
199
+ const error = err && typeof err === "object" && err.code
200
+ ? err
201
+ : buildStructuredError("tool_execution_failed", err && err.message ? err.message : String(err || "tool failed"));
202
+
203
+ observer.emit("tool_call_finished", {
204
+ tool_name: name,
205
+ tool_call_id: toolCallId,
206
+ turn_id: turnId,
207
+ ok: false,
208
+ error_code: error.code,
209
+ });
210
+ observer.audit({
211
+ source: "controller_loop",
212
+ kind: "tool_call",
213
+ tool_name: name,
214
+ tool_call_id: toolCallId,
215
+ turn_id: turnId,
216
+ ok: false,
217
+ error,
218
+ });
219
+
220
+ return {
221
+ ok: false,
222
+ name,
223
+ tool_call_id: toolCallId,
224
+ turn_id: turnId,
225
+ error,
226
+ };
227
+ }
228
+ }
229
+
230
+ module.exports = {
231
+ buildStructuredError,
232
+ executeControllerTool,
233
+ };