@yagr/agent 0.2.11 → 0.2.13

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 (212) hide show
  1. package/dist/agent.d.ts +16 -12
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +40 -33
  4. package/dist/agent.js.map +1 -1
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +109 -17
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/n8n-config-service.d.ts +16 -0
  10. package/dist/config/n8n-config-service.d.ts.map +1 -1
  11. package/dist/config/n8n-config-service.js +32 -1
  12. package/dist/config/n8n-config-service.js.map +1 -1
  13. package/dist/config/yagr-config-service.d.ts +16 -0
  14. package/dist/config/yagr-config-service.d.ts.map +1 -1
  15. package/dist/config/yagr-config-service.js.map +1 -1
  16. package/dist/engine/engine.d.ts +15 -1
  17. package/dist/engine/engine.d.ts.map +1 -1
  18. package/dist/gateway/cli.d.ts +2 -2
  19. package/dist/gateway/cli.d.ts.map +1 -1
  20. package/dist/gateway/cli.js +6 -3
  21. package/dist/gateway/cli.js.map +1 -1
  22. package/dist/gateway/format-message.d.ts +8 -4
  23. package/dist/gateway/format-message.d.ts.map +1 -1
  24. package/dist/gateway/format-message.js +10 -5
  25. package/dist/gateway/format-message.js.map +1 -1
  26. package/dist/gateway/interactive-ui.d.ts +2 -2
  27. package/dist/gateway/interactive-ui.d.ts.map +1 -1
  28. package/dist/gateway/interactive-ui.js +87 -82
  29. package/dist/gateway/interactive-ui.js.map +1 -1
  30. package/dist/gateway/manager.d.ts +6 -6
  31. package/dist/gateway/manager.d.ts.map +1 -1
  32. package/dist/gateway/manager.js.map +1 -1
  33. package/dist/gateway/telegram.d.ts +5 -5
  34. package/dist/gateway/telegram.d.ts.map +1 -1
  35. package/dist/gateway/telegram.js +100 -101
  36. package/dist/gateway/telegram.js.map +1 -1
  37. package/dist/gateway/webui.d.ts +52 -5
  38. package/dist/gateway/webui.d.ts.map +1 -1
  39. package/dist/gateway/webui.js +109 -237
  40. package/dist/gateway/webui.js.map +1 -1
  41. package/dist/gateway/workflow-diagram.d.ts +19 -0
  42. package/dist/gateway/workflow-diagram.d.ts.map +1 -0
  43. package/dist/gateway/workflow-diagram.js +124 -0
  44. package/dist/gateway/workflow-diagram.js.map +1 -0
  45. package/dist/index.d.ts +9 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +6 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/llm/anthropic-account.d.ts +2 -1
  50. package/dist/llm/anthropic-account.d.ts.map +1 -1
  51. package/dist/llm/anthropic-account.js +1 -1
  52. package/dist/llm/anthropic-account.js.map +1 -1
  53. package/dist/llm/capability-resolver.d.ts +10 -0
  54. package/dist/llm/capability-resolver.d.ts.map +1 -0
  55. package/dist/llm/capability-resolver.js +93 -0
  56. package/dist/llm/capability-resolver.js.map +1 -0
  57. package/dist/llm/copilot-account.d.ts +2 -1
  58. package/dist/llm/copilot-account.d.ts.map +1 -1
  59. package/dist/llm/copilot-account.js +323 -32
  60. package/dist/llm/copilot-account.js.map +1 -1
  61. package/dist/llm/create-language-model.d.ts.map +1 -1
  62. package/dist/llm/create-language-model.js +12 -171
  63. package/dist/llm/create-language-model.js.map +1 -1
  64. package/dist/llm/model-capabilities.d.ts +32 -0
  65. package/dist/llm/model-capabilities.d.ts.map +1 -0
  66. package/dist/llm/model-capabilities.js +144 -0
  67. package/dist/llm/model-capabilities.js.map +1 -0
  68. package/dist/llm/openai-account.d.ts +2 -1
  69. package/dist/llm/openai-account.d.ts.map +1 -1
  70. package/dist/llm/openai-account.js +29 -17
  71. package/dist/llm/openai-account.js.map +1 -1
  72. package/dist/llm/provider-discovery.d.ts +1 -1
  73. package/dist/llm/provider-discovery.d.ts.map +1 -1
  74. package/dist/llm/provider-discovery.js +3 -34
  75. package/dist/llm/provider-discovery.js.map +1 -1
  76. package/dist/llm/provider-metadata.d.ts +27 -0
  77. package/dist/llm/provider-metadata.d.ts.map +1 -0
  78. package/dist/llm/provider-metadata.js +327 -0
  79. package/dist/llm/provider-metadata.js.map +1 -0
  80. package/dist/llm/provider-plugin.d.ts +41 -0
  81. package/dist/llm/provider-plugin.d.ts.map +1 -0
  82. package/dist/llm/provider-plugin.js +429 -0
  83. package/dist/llm/provider-plugin.js.map +1 -0
  84. package/dist/llm/provider-registry.d.ts +5 -1
  85. package/dist/llm/provider-registry.d.ts.map +1 -1
  86. package/dist/llm/provider-registry.js +7 -24
  87. package/dist/llm/provider-registry.js.map +1 -1
  88. package/dist/llm/proxy-runtime.d.ts.map +1 -1
  89. package/dist/llm/proxy-runtime.js +16 -63
  90. package/dist/llm/proxy-runtime.js.map +1 -1
  91. package/dist/llm/test-model-policy.d.ts.map +1 -1
  92. package/dist/llm/test-model-policy.js +8 -10
  93. package/dist/llm/test-model-policy.js.map +1 -1
  94. package/dist/llm/tool-schema.d.ts +4 -0
  95. package/dist/llm/tool-schema.d.ts.map +1 -0
  96. package/dist/llm/tool-schema.js +80 -0
  97. package/dist/llm/tool-schema.js.map +1 -0
  98. package/dist/prompt/build-system-prompt.d.ts +3 -3
  99. package/dist/prompt/build-system-prompt.d.ts.map +1 -1
  100. package/dist/prompt/build-system-prompt.js +2 -0
  101. package/dist/prompt/build-system-prompt.js.map +1 -1
  102. package/dist/runtime/completion-gate.d.ts +4 -0
  103. package/dist/runtime/completion-gate.d.ts.map +1 -1
  104. package/dist/runtime/completion-gate.js +13 -2
  105. package/dist/runtime/completion-gate.js.map +1 -1
  106. package/dist/runtime/context-compaction.d.ts.map +1 -1
  107. package/dist/runtime/context-compaction.js +6 -5
  108. package/dist/runtime/context-compaction.js.map +1 -1
  109. package/dist/runtime/outcome.d.ts +3 -0
  110. package/dist/runtime/outcome.d.ts.map +1 -1
  111. package/dist/runtime/outcome.js +40 -3
  112. package/dist/runtime/outcome.js.map +1 -1
  113. package/dist/runtime/policy-hooks.d.ts +4 -0
  114. package/dist/runtime/policy-hooks.d.ts.map +1 -1
  115. package/dist/runtime/policy-hooks.js +137 -0
  116. package/dist/runtime/policy-hooks.js.map +1 -1
  117. package/dist/runtime/required-actions.d.ts +5 -0
  118. package/dist/runtime/required-actions.d.ts.map +1 -1
  119. package/dist/runtime/required-actions.js +22 -4
  120. package/dist/runtime/required-actions.js.map +1 -1
  121. package/dist/runtime/run-engine.d.ts +6 -3
  122. package/dist/runtime/run-engine.d.ts.map +1 -1
  123. package/dist/runtime/run-engine.js +699 -97
  124. package/dist/runtime/run-engine.js.map +1 -1
  125. package/dist/runtime/tool-runtime-strategy.d.ts +29 -0
  126. package/dist/runtime/tool-runtime-strategy.d.ts.map +1 -0
  127. package/dist/runtime/tool-runtime-strategy.js +102 -0
  128. package/dist/runtime/tool-runtime-strategy.js.map +1 -0
  129. package/dist/runtime/user-visible-updates.d.ts +12 -0
  130. package/dist/runtime/user-visible-updates.d.ts.map +1 -0
  131. package/dist/runtime/user-visible-updates.js +93 -0
  132. package/dist/runtime/user-visible-updates.js.map +1 -0
  133. package/dist/setup/application-services.d.ts +199 -0
  134. package/dist/setup/application-services.d.ts.map +1 -0
  135. package/dist/setup/application-services.js +468 -0
  136. package/dist/setup/application-services.js.map +1 -0
  137. package/dist/setup/setup-wizard.d.ts +1 -0
  138. package/dist/setup/setup-wizard.d.ts.map +1 -1
  139. package/dist/setup/setup-wizard.js +16 -18
  140. package/dist/setup/setup-wizard.js.map +1 -1
  141. package/dist/setup/status.d.ts +21 -0
  142. package/dist/setup/status.d.ts.map +1 -0
  143. package/dist/setup/status.js +47 -0
  144. package/dist/setup/status.js.map +1 -0
  145. package/dist/setup.d.ts +3 -14
  146. package/dist/setup.d.ts.map +1 -1
  147. package/dist/setup.js +30 -256
  148. package/dist/setup.js.map +1 -1
  149. package/dist/tools/build-tools.d.ts +160 -18
  150. package/dist/tools/build-tools.d.ts.map +1 -1
  151. package/dist/tools/build-tools.js +11 -1
  152. package/dist/tools/build-tools.js.map +1 -1
  153. package/dist/tools/deploy.d.ts +2 -2
  154. package/dist/tools/deploy.d.ts.map +1 -1
  155. package/dist/tools/deploy.js.map +1 -1
  156. package/dist/tools/generate-workflow.d.ts +9 -9
  157. package/dist/tools/generate-workflow.d.ts.map +1 -1
  158. package/dist/tools/generate-workflow.js.map +1 -1
  159. package/dist/tools/index.d.ts +1 -1
  160. package/dist/tools/index.d.ts.map +1 -1
  161. package/dist/tools/index.js.map +1 -1
  162. package/dist/tools/list-workflows.d.ts +2 -2
  163. package/dist/tools/list-workflows.d.ts.map +1 -1
  164. package/dist/tools/list-workflows.js.map +1 -1
  165. package/dist/tools/manage-workflow.d.ts +2 -2
  166. package/dist/tools/manage-workflow.d.ts.map +1 -1
  167. package/dist/tools/manage-workflow.js.map +1 -1
  168. package/dist/tools/n8nac.d.ts +121 -4
  169. package/dist/tools/n8nac.d.ts.map +1 -1
  170. package/dist/tools/n8nac.js +183 -38
  171. package/dist/tools/n8nac.js.map +1 -1
  172. package/dist/tools/node-info.d.ts +2 -2
  173. package/dist/tools/node-info.d.ts.map +1 -1
  174. package/dist/tools/node-info.js.map +1 -1
  175. package/dist/tools/observer.d.ts +6 -35
  176. package/dist/tools/observer.d.ts.map +1 -1
  177. package/dist/tools/observer.js +18 -0
  178. package/dist/tools/observer.js.map +1 -1
  179. package/dist/tools/present-workflow-result.d.ts +1 -0
  180. package/dist/tools/present-workflow-result.d.ts.map +1 -1
  181. package/dist/tools/present-workflow-result.js +24 -5
  182. package/dist/tools/present-workflow-result.js.map +1 -1
  183. package/dist/tools/request-required-action.d.ts +7 -3
  184. package/dist/tools/request-required-action.d.ts.map +1 -1
  185. package/dist/tools/request-required-action.js +5 -3
  186. package/dist/tools/request-required-action.js.map +1 -1
  187. package/dist/tools/search-nodes.d.ts +2 -2
  188. package/dist/tools/search-nodes.d.ts.map +1 -1
  189. package/dist/tools/search-nodes.js.map +1 -1
  190. package/dist/tools/search-templates.d.ts +2 -2
  191. package/dist/tools/search-templates.d.ts.map +1 -1
  192. package/dist/tools/search-templates.js.map +1 -1
  193. package/dist/tools/toolsets.d.ts +11 -0
  194. package/dist/tools/toolsets.d.ts.map +1 -0
  195. package/dist/tools/toolsets.js +49 -0
  196. package/dist/tools/toolsets.js.map +1 -0
  197. package/dist/tools/validate.d.ts +2 -2
  198. package/dist/tools/validate.d.ts.map +1 -1
  199. package/dist/tools/validate.js.map +1 -1
  200. package/dist/tools/write-workspace-file.d.ts +25 -9
  201. package/dist/tools/write-workspace-file.d.ts.map +1 -1
  202. package/dist/tools/write-workspace-file.js +27 -4
  203. package/dist/tools/write-workspace-file.js.map +1 -1
  204. package/dist/types.d.ts +1 -0
  205. package/dist/types.d.ts.map +1 -1
  206. package/dist/webui/app.js +97 -101
  207. package/dist/webui/app.js.map +4 -4
  208. package/package.json +6 -5
  209. package/dist/llm/google-account.d.ts +0 -31
  210. package/dist/llm/google-account.d.ts.map +0 -1
  211. package/dist/llm/google-account.js +0 -851
  212. package/dist/llm/google-account.js.map +0 -1
@@ -1,17 +1,20 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { generateText, streamText } from 'ai';
2
+ import { generateText, streamText, InvalidToolArgumentsError } from 'ai';
3
3
  import { createLanguageModel } from '../llm/create-language-model.js';
4
- import { resolveModelContextProfile } from '../llm/create-language-model.js';
4
+ import { resolveLanguageModelConfig, resolveModelContextProfile } from '../llm/create-language-model.js';
5
+ import { getProviderPlugin } from '../llm/provider-plugin.js';
5
6
  import { buildTools } from '../tools/index.js';
7
+ import { resolveWorkflowOpenLink } from '../gateway/workflow-links.js';
8
+ import { resolveWorkflowDiagramFromFilePath } from '../tools/present-workflow-result.js';
6
9
  import { evaluateCompletionGate } from './completion-gate.js';
7
10
  import { compactConversationContext } from './context-compaction.js';
8
11
  import { analyzeRunOutcome, formatObservedAction } from './outcome.js';
9
- import { createDefaultRuntimeHooks, wrapToolsWithRuntimeHooks } from './policy-hooks.js';
10
- import { blockingStateForRequiredActions, collectRequiredActions } from './required-actions.js';
11
- const INSPECT_MAX_STEPS = 4;
12
- const EXECUTE_MAX_STEPS = 10;
13
- const EXECUTE_RECOVERY_MAX_STEPS = 6;
12
+ import { createDefaultRuntimeHooksForStrategy, wrapToolsWithRuntimeHooks } from './policy-hooks.js';
13
+ import { blockingStateForRequiredActions, collectRequiredActions, splitRequiredActions } from './required-actions.js';
14
+ import { resolveToolRuntimeStrategy } from './tool-runtime-strategy.js';
14
15
  const MAX_EXECUTION_ATTEMPTS = 3;
16
+ const MAX_COMPLETION_REPAIR_ATTEMPTS = 1;
17
+ const MAX_BLOCKER_CAPTURE_ATTEMPTS = 1;
15
18
  const PHASE_ORDER = ['inspect', 'plan', 'edit', 'validate', 'sync', 'verify', 'summarize'];
16
19
  const STREAM_FILTER_HOLDBACK = 256;
17
20
  /**
@@ -33,11 +36,11 @@ class RepetitiveAssistantOutputError extends Error {
33
36
  function isRepetitiveAssistantOutputError(error) {
34
37
  return error instanceof RepetitiveAssistantOutputError;
35
38
  }
36
- function providerOptionsForRun(provider) {
37
- if (provider === 'openai' || provider === 'groq') {
38
- return { openai: { strictSchemas: false } };
39
- }
40
- return undefined;
39
+ function isSyntheticWriteWorkspaceFileIntent(intent) {
40
+ return intent.tool === 'writeWorkspaceFile';
41
+ }
42
+ function isSyntheticN8nacIntent(intent) {
43
+ return intent.tool !== 'writeWorkspaceFile';
41
44
  }
42
45
  function createAbortError(message = 'Yagr run stopped by user.') {
43
46
  const error = new Error(message);
@@ -59,25 +62,184 @@ function throwIfAborted(signal) {
59
62
  function wrapInternal(text) {
60
63
  return `${INTERNAL_TAG_OPEN}${text}${INTERNAL_TAG_CLOSE}`;
61
64
  }
62
- function createPhasePrompt(phase, userPrompt) {
65
+ function looksLikeRawToolIntentText(text) {
66
+ const trimmed = text.trim();
67
+ if (!trimmed.startsWith('{')) {
68
+ return false;
69
+ }
70
+ const blocks = extractJsonObjectBlocks(trimmed);
71
+ if (blocks.length === 0) {
72
+ return false;
73
+ }
74
+ return blocks.join('') === trimmed && blocks.every((block) => {
75
+ try {
76
+ const parsed = JSON.parse(block);
77
+ return typeof parsed.action === 'string' || parsed.tool === 'writeWorkspaceFile';
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ });
83
+ }
84
+ function extractJsonObjectBlocks(text) {
85
+ const blocks = [];
86
+ let depth = 0;
87
+ let inString = false;
88
+ let escaping = false;
89
+ let start = -1;
90
+ for (let index = 0; index < text.length; index += 1) {
91
+ const character = text[index];
92
+ if (escaping) {
93
+ escaping = false;
94
+ continue;
95
+ }
96
+ if (character === '\\') {
97
+ escaping = true;
98
+ continue;
99
+ }
100
+ if (character === '"') {
101
+ inString = !inString;
102
+ continue;
103
+ }
104
+ if (inString) {
105
+ continue;
106
+ }
107
+ if (character === '{') {
108
+ if (depth === 0) {
109
+ start = index;
110
+ }
111
+ depth += 1;
112
+ continue;
113
+ }
114
+ if (character === '}') {
115
+ depth -= 1;
116
+ if (depth === 0 && start >= 0) {
117
+ blocks.push(text.slice(start, index + 1));
118
+ start = -1;
119
+ }
120
+ }
121
+ }
122
+ return blocks;
123
+ }
124
+ function parseSyntheticToolIntents(text) {
125
+ const intents = [];
126
+ for (const block of extractJsonObjectBlocks(text)) {
127
+ try {
128
+ const parsed = JSON.parse(block);
129
+ if (parsed.tool === 'writeWorkspaceFile') {
130
+ if (typeof parsed.path !== 'string' || typeof parsed.content !== 'string') {
131
+ continue;
132
+ }
133
+ intents.push({
134
+ tool: 'writeWorkspaceFile',
135
+ path: parsed.path,
136
+ content: parsed.content,
137
+ mode: parsed.mode === 'create' || parsed.mode === 'append' ? parsed.mode : 'overwrite',
138
+ });
139
+ continue;
140
+ }
141
+ if (typeof parsed.action !== 'string') {
142
+ continue;
143
+ }
144
+ intents.push({
145
+ tool: typeof parsed.tool === 'string' ? parsed.tool : 'n8nac',
146
+ action: parsed.action,
147
+ filename: typeof parsed.filename === 'string' ? parsed.filename : undefined,
148
+ validateFile: typeof parsed.validateFile === 'string' ? parsed.validateFile : undefined,
149
+ workflowId: typeof parsed.workflowId === 'string' ? parsed.workflowId : undefined,
150
+ listScope: typeof parsed.listScope === 'string' ? parsed.listScope : undefined,
151
+ n8nHost: typeof parsed.n8nHost === 'string' ? parsed.n8nHost : undefined,
152
+ n8nApiKey: typeof parsed.n8nApiKey === 'string' || parsed.n8nApiKey === null ? parsed.n8nApiKey : undefined,
153
+ projectId: typeof parsed.projectId === 'string' || parsed.projectId === null ? parsed.projectId : undefined,
154
+ projectName: typeof parsed.projectName === 'string' || parsed.projectName === null ? parsed.projectName : undefined,
155
+ projectIndex: typeof parsed.projectIndex === 'number' ? parsed.projectIndex : undefined,
156
+ skillsArgs: typeof parsed.skillsArgs === 'string' || parsed.skillsArgs === null ? parsed.skillsArgs : undefined,
157
+ skillsArgv: Array.isArray(parsed.skillsArgv) ? parsed.skillsArgv.filter((value) => typeof value === 'string') : undefined,
158
+ syncFolder: typeof parsed.syncFolder === 'string' || parsed.syncFolder === null ? parsed.syncFolder : undefined,
159
+ resolveMode: typeof parsed.resolveMode === 'string' || parsed.resolveMode === null ? parsed.resolveMode : undefined,
160
+ });
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ }
166
+ return intents;
167
+ }
168
+ async function maybeExecuteSyntheticToolIntents(state, options, strategy, tools, phaseResult) {
169
+ if (!['weak', 'none'].includes(strategy.capabilityProfile.toolCalling)) {
170
+ return false;
171
+ }
172
+ if (phaseResult.toolCalls.length > 0) {
173
+ return false;
174
+ }
175
+ const intents = parseSyntheticToolIntents(phaseResult.text)
176
+ .filter((intent) => (isSyntheticWriteWorkspaceFileIntent(intent)
177
+ || (isSyntheticN8nacIntent(intent) && ['validate', 'push', 'verify'].includes(intent.action))))
178
+ .slice(0, strategy.capabilityProfile.toolCalling === 'none' ? 4 : 3);
179
+ if (intents.length === 0) {
180
+ return false;
181
+ }
182
+ for (const intent of intents) {
183
+ if (isSyntheticWriteWorkspaceFileIntent(intent)) {
184
+ const writeTool = tools.writeWorkspaceFile;
185
+ const args = {
186
+ path: intent.path,
187
+ content: intent.content,
188
+ mode: intent.mode ?? 'overwrite',
189
+ };
190
+ const result = await writeTool.execute(args, undefined);
191
+ await recordStep(state, options, {
192
+ stepType: 'tool-result',
193
+ finishReason: 'tool-calls',
194
+ toolCalls: [{ toolName: 'writeWorkspaceFile', args }],
195
+ toolResults: [{ toolName: 'writeWorkspaceFile', result }],
196
+ text: '',
197
+ });
198
+ continue;
199
+ }
200
+ if (!isSyntheticN8nacIntent(intent)) {
201
+ continue;
202
+ }
203
+ const args = {
204
+ ...intent,
205
+ validateFile: intent.validateFile || (intent.action === 'validate' ? intent.filename : undefined),
206
+ };
207
+ const n8nacTool = tools.n8nac;
208
+ const result = await n8nacTool.execute(args, undefined);
209
+ await recordStep(state, options, {
210
+ stepType: 'tool-result',
211
+ finishReason: 'tool-calls',
212
+ toolCalls: [{ toolName: 'n8nac', args }],
213
+ toolResults: [{ toolName: 'n8nac', result }],
214
+ text: '',
215
+ });
216
+ }
217
+ return true;
218
+ }
219
+ function createPhasePrompt(phase, userPrompt, strategy) {
220
+ const toolUseInstruction = strategy.tooling.toolCallMode === 'disabled'
221
+ ? 'Do not call tools in this phase. Reason from the current context only.'
222
+ : 'Use tools to inspect the workspace, existing workflow files, examples, and n8nac workspace status when needed.';
63
223
  if (phase === 'inspect') {
64
224
  return wrapInternal([
65
225
  'Yagr internal phase: inspect.',
66
226
  'Analyze the request and gather only the context needed to execute it correctly.',
67
- 'Use tools to inspect the workspace, existing workflow files, examples, and n8nac workspace status when needed.',
227
+ toolUseInstruction,
68
228
  'Do not reread the workspace AGENT.md or AGENTS.md file during inspect unless a specific later detail is missing from the current context.',
69
229
  'Favor correctness over speed in this phase. If an example or rule is likely to determine the right implementation, read it before acting.',
70
230
  'Do not claim completion in this phase.',
231
+ ...strategy.inspectDirectives,
71
232
  `Original request: ${userPrompt}`,
72
233
  ].join('\n'));
73
234
  }
74
- return wrapInternal([
75
- 'Yagr internal phase: execute.',
76
- 'Complete the task end-to-end using the gathered context.',
77
- 'When appropriate, author or edit workflow files, validate them, sync them, verify them, and then summarize what changed.',
235
+ return [
236
+ 'Complete the following task end-to-end.',
237
+ 'Author or edit workflow files, validate them, push them to n8n, and verify the deployment.',
238
+ 'You must call at least one tool to make progress do not respond with text alone.',
78
239
  'Ask the user only when a specific missing value blocks execution.',
79
- `Original request: ${userPrompt}`,
80
- ].join('\n'));
240
+ ...strategy.executeDirectives,
241
+ `Task: ${userPrompt}`,
242
+ ].join('\n');
81
243
  }
82
244
  function trimAssistantVisibleText(text, maxChars = 12_000) {
83
245
  return text.length <= maxChars ? text : text.slice(-maxChars);
@@ -265,46 +427,306 @@ function collectToolNames(journal) {
265
427
  }
266
428
  return [...names].map((toolName) => ({ toolName }));
267
429
  }
268
- function buildGroundedSummary(_prompt, finishReason, journal, requiredActions) {
430
+ function hasObservedToolCall(journal, toolNames) {
431
+ const wanted = new Set(toolNames);
432
+ for (const entry of journal) {
433
+ if (entry.type !== 'step' || !entry.step) {
434
+ continue;
435
+ }
436
+ if (entry.step.toolCalls.some((toolCall) => wanted.has(toolCall.toolName))) {
437
+ return true;
438
+ }
439
+ }
440
+ return false;
441
+ }
442
+ function collectPresentedWorkflow(result) {
443
+ if (!result || typeof result !== 'object') {
444
+ return undefined;
445
+ }
446
+ const record = result;
447
+ const workflowId = typeof record.workflowId === 'string' ? record.workflowId : undefined;
448
+ const workflowUrl = typeof record.workflowUrl === 'string' ? record.workflowUrl : undefined;
449
+ const title = typeof record.title === 'string' ? record.title : undefined;
450
+ if (!workflowId && !workflowUrl && !title) {
451
+ return undefined;
452
+ }
453
+ return { workflowId, workflowUrl, title };
454
+ }
455
+ function collectWorkflowPresentationFromOutcome(outcome) {
456
+ const workflowId = outcome.successfulVerify?.workflowId ?? outcome.successfulPush?.workflowId;
457
+ const workflowUrl = outcome.successfulVerify?.workflowUrl ?? outcome.successfulPush?.workflowUrl;
458
+ const title = outcome.successfulVerify?.title ?? outcome.successfulPush?.title;
459
+ if (!workflowId && !workflowUrl && !title) {
460
+ return undefined;
461
+ }
462
+ return { workflowId, workflowUrl, title };
463
+ }
464
+ function extractWorkflowLabel(outcome, journal) {
465
+ const successfulPushTarget = outcome.successfulPush?.filename;
466
+ const workflowFilePath = successfulPushTarget
467
+ || outcome.writtenFiles.find((filePath) => filePath.endsWith('.workflow.ts'))
468
+ || outcome.updatedFiles.find((filePath) => filePath.endsWith('.workflow.ts'));
469
+ if (!workflowFilePath) {
470
+ return undefined;
471
+ }
472
+ const baseName = workflowFilePath.split('/').pop() ?? workflowFilePath;
473
+ return baseName.replace(/\.workflow\.ts$/i, '');
474
+ }
475
+ function extractPresentedWorkflowFromJournal(journal) {
476
+ for (let index = journal.length - 1; index >= 0; index -= 1) {
477
+ const entry = journal[index];
478
+ if (entry.type !== 'step' || !entry.step) {
479
+ continue;
480
+ }
481
+ for (let toolIndex = entry.step.toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
482
+ if (entry.step.toolCalls[toolIndex]?.toolName !== 'presentWorkflowResult') {
483
+ continue;
484
+ }
485
+ const presented = collectPresentedWorkflow(entry.step.toolResults[toolIndex]?.result);
486
+ if (presented) {
487
+ return presented;
488
+ }
489
+ }
490
+ }
491
+ return undefined;
492
+ }
493
+ async function maybeEmitSyntheticWorkflowEmbed(outcome, journal, onToolEvent) {
494
+ if (!onToolEvent) {
495
+ return;
496
+ }
497
+ const presentedWorkflow = extractPresentedWorkflowFromJournal(journal);
498
+ if (presentedWorkflow?.workflowUrl) {
499
+ return;
500
+ }
501
+ const fallbackWorkflow = collectWorkflowPresentationFromOutcome(outcome);
502
+ if (!fallbackWorkflow?.workflowId || !fallbackWorkflow.workflowUrl) {
503
+ return;
504
+ }
505
+ const workflowLink = resolveWorkflowOpenLink(fallbackWorkflow.workflowUrl);
506
+ const pushTarget = outcome.successfulPush?.filename;
507
+ const diagram = (pushTarget ? resolveWorkflowDiagramFromFilePath(pushTarget) : undefined)
508
+ || [
509
+ '<workflow-map>',
510
+ `// Workflow : ${fallbackWorkflow.title || fallbackWorkflow.workflowId || 'Workflow'}`,
511
+ '// ROUTING MAP',
512
+ '// Diagram unavailable in source; link card synthesized from successful push/verify facts.',
513
+ '</workflow-map>',
514
+ ].join('\n');
515
+ await onToolEvent({
516
+ type: 'embed',
517
+ toolName: 'presentWorkflowResult',
518
+ kind: 'workflow',
519
+ workflowId: fallbackWorkflow.workflowId,
520
+ url: workflowLink.openUrl,
521
+ targetUrl: workflowLink.targetUrl,
522
+ title: fallbackWorkflow.title,
523
+ diagram,
524
+ });
525
+ }
526
+ export function buildGroundedSummary(_prompt, finishReason, journal, requiredActions) {
269
527
  const lines = [];
270
528
  const outcome = analyzeRunOutcome(journal);
271
- if (outcome.writtenFiles.length > 0) {
272
- lines.push(`Fichiers crees ou reecrits: ${outcome.writtenFiles.join(', ')}`);
529
+ const { blocking: blockingRequiredActions, followUp: followUpRequiredActions } = splitRequiredActions(requiredActions);
530
+ const workflowLabel = extractWorkflowLabel(outcome, journal);
531
+ const presentedWorkflow = extractPresentedWorkflowFromJournal(journal) ?? collectWorkflowPresentationFromOutcome(outcome);
532
+ const presentedWorkflowUrl = presentedWorkflow?.workflowUrl;
533
+ if (outcome.hasWorkflowWrites && outcome.successfulPush) {
534
+ const workflowName = presentedWorkflow?.title || workflowLabel || 'the workflow';
535
+ const completionBits = [
536
+ `The workflow ${workflowName === 'the workflow' ? workflowName : `\`${workflowName}\``} is ready`,
537
+ outcome.successfulValidate ? 'validated' : undefined,
538
+ outcome.successfulPush ? 'pushed to n8n' : undefined,
539
+ outcome.successfulVerify ? 'verified' : undefined,
540
+ ].filter(Boolean);
541
+ if (completionBits.length > 0) {
542
+ lines.push(`${completionBits.join(', ')}.`);
543
+ }
544
+ if (presentedWorkflowUrl) {
545
+ lines.push(`Workflow link: ${presentedWorkflowUrl}`);
546
+ }
273
547
  }
274
- if (outcome.updatedFiles.length > 0) {
275
- lines.push(`Fichiers modifies: ${outcome.updatedFiles.join(', ')}`);
548
+ if (lines.length === 0 && presentedWorkflowUrl) {
549
+ const workflowName = presentedWorkflow.title || workflowLabel || 'the workflow';
550
+ lines.push(workflowName === 'the workflow'
551
+ ? 'The workflow is ready.'
552
+ : `The workflow \`${workflowName}\` is ready.`);
553
+ lines.push(`Workflow link: ${presentedWorkflowUrl}`);
276
554
  }
277
- if (outcome.successfulActions.length > 0) {
278
- lines.push(`Actions n8nac reussies: ${outcome.successfulActions.map(formatObservedAction).join(', ')}`);
555
+ else if (lines.length === 0 && presentedWorkflow?.title) {
556
+ lines.push(`The workflow \`${presentedWorkflow.title}\` is ready.`);
279
557
  }
280
- if (outcome.unresolvedFailedActions.length > 0) {
281
- lines.push(`Actions n8nac en echec: ${outcome.unresolvedFailedActions.map(formatObservedAction).join(', ')}`);
558
+ if (lines.length > 0 && presentedWorkflowUrl && !lines.some((line) => line.includes(presentedWorkflowUrl))) {
559
+ lines.push(`Workflow link: ${presentedWorkflowUrl}`);
282
560
  }
283
- if (requiredActions.length > 0) {
284
- lines.push(`Actions requises en attente: ${requiredActions.map((action) => `${action.title} [${action.kind}]`).join(', ')}`);
561
+ if (lines.length === 0 && outcome.writtenFiles.length > 0) {
562
+ lines.push('The task changed local files, but the final result is not fully confirmed yet.');
563
+ }
564
+ if (lines.length === 0 && outcome.updatedFiles.length > 0) {
565
+ lines.push('The task updated local files, but the final result is not fully confirmed yet.');
566
+ }
567
+ if (outcome.blockingUnresolvedFailedActions.length > 0) {
568
+ lines.push('The run stopped because some execution steps still need correction.');
569
+ }
570
+ if (blockingRequiredActions.length > 0 && !(outcome.successfulPush && outcome.successfulVerify)) {
571
+ lines.push(`Pending required actions: ${blockingRequiredActions.map((action) => action.title).join(', ')}`);
572
+ }
573
+ if (followUpRequiredActions.length > 0 && (outcome.successfulPush || outcome.successfulVerify || presentedWorkflowUrl)) {
574
+ lines.push(`Next steps: ${followUpRequiredActions.map((action) => action.title).join(', ')}`);
285
575
  }
286
576
  if (outcome.hasWorkflowWrites && !outcome.successfulValidate) {
287
- lines.push('La validation du workflow n’a pas ete confirmee.');
577
+ lines.push('Workflow validation was not confirmed.');
288
578
  }
289
579
  if (outcome.hasWorkflowWrites && !outcome.successfulPush) {
290
- lines.push('Le push vers n8n n’a pas ete confirme.');
580
+ lines.push('Push to n8n was not confirmed.');
291
581
  }
292
582
  if (outcome.hasWorkflowWrites && !outcome.successfulVerify) {
293
- lines.push('La verification distante n’a pas ete confirmee.');
583
+ lines.push('Remote verification was not confirmed.');
584
+ }
585
+ if (outcome.hasWorkflowWrites && outcome.blockingUnresolvedFailedActions.length > 0) {
586
+ lines.push('The run stopped while some actions were still failing. More fixes are needed or an external blocker is still present.');
587
+ }
588
+ if (lines.length === 0 && finishReason !== 'stop') {
589
+ lines.push(`The run ended with reason: ${finishReason}.`);
590
+ }
591
+ if (lines.length === 0) {
592
+ lines.push('The task did not reach a grounded terminal result yet.');
593
+ }
594
+ return lines.join('\n');
595
+ }
596
+ export function shouldForceGroundedFinalAnswer(journal, requiredActions = []) {
597
+ const outcome = analyzeRunOutcome(journal);
598
+ const presentedWorkflow = extractPresentedWorkflowFromJournal(journal) ?? collectWorkflowPresentationFromOutcome(outcome);
599
+ const { blocking: blockingRequiredActions, followUp: followUpRequiredActions } = splitRequiredActions(requiredActions);
600
+ if (blockingRequiredActions.length > 0) {
601
+ return true;
602
+ }
603
+ if (presentedWorkflow?.workflowUrl) {
604
+ return true;
605
+ }
606
+ if (followUpRequiredActions.length > 0) {
607
+ return true;
608
+ }
609
+ return Boolean(outcome.hasWorkflowWrites && (outcome.successfulPush || outcome.successfulVerify));
610
+ }
611
+ export function finalAnswerSatisfiesGroundedWorkflowFacts(text, journal) {
612
+ const normalizedText = sanitizeAssistantOutput(text);
613
+ if (!normalizedText) {
614
+ return false;
615
+ }
616
+ const outcome = analyzeRunOutcome(journal);
617
+ const presentedWorkflow = extractPresentedWorkflowFromJournal(journal) ?? collectWorkflowPresentationFromOutcome(outcome);
618
+ if (presentedWorkflow?.workflowUrl && !normalizedText.includes(presentedWorkflow.workflowUrl)) {
619
+ return false;
620
+ }
621
+ return true;
622
+ }
623
+ function buildFinalAnswerFacts(finishReason, journal, requiredActions) {
624
+ const outcome = analyzeRunOutcome(journal);
625
+ const { blocking: blockingRequiredActions, followUp: followUpRequiredActions } = splitRequiredActions(requiredActions);
626
+ const workflowLabel = extractWorkflowLabel(outcome, journal);
627
+ const presentedWorkflow = extractPresentedWorkflowFromJournal(journal) ?? collectWorkflowPresentationFromOutcome(outcome);
628
+ const lines = [];
629
+ lines.push(`finish_reason=${finishReason}`);
630
+ lines.push(`workflow_writes=${outcome.hasWorkflowWrites ? 'yes' : 'no'}`);
631
+ lines.push(`validate_confirmed=${outcome.successfulValidate ? 'yes' : 'no'}`);
632
+ lines.push(`push_confirmed=${outcome.successfulPush ? 'yes' : 'no'}`);
633
+ lines.push(`verify_confirmed=${outcome.successfulVerify ? 'yes' : 'no'}`);
634
+ if (workflowLabel) {
635
+ lines.push(`workflow_label=${workflowLabel}`);
636
+ }
637
+ if (presentedWorkflow?.title) {
638
+ lines.push(`workflow_title=${presentedWorkflow.title}`);
639
+ }
640
+ if (presentedWorkflow?.workflowUrl) {
641
+ lines.push(`workflow_url=${presentedWorkflow.workflowUrl}`);
642
+ }
643
+ if (outcome.writtenFiles.length > 0) {
644
+ lines.push(`written_files=${outcome.writtenFiles.join(', ')}`);
645
+ }
646
+ if (outcome.updatedFiles.length > 0) {
647
+ lines.push(`updated_files=${outcome.updatedFiles.join(', ')}`);
648
+ }
649
+ if (outcome.successfulActions.length > 0) {
650
+ lines.push(`successful_actions=${outcome.successfulActions.map(formatObservedAction).join(', ')}`);
294
651
  }
295
- if (outcome.hasWorkflowWrites && outcome.unresolvedFailedActions.length > 0) {
296
- lines.push('Le run s’est arrete alors que certaines actions avaient encore echoue. Une correction supplementaire reste necessaire ou un bloqueur externe persiste.');
652
+ if (outcome.blockingUnresolvedFailedActions.length > 0) {
653
+ lines.push(`blocking_failed_actions=${outcome.blockingUnresolvedFailedActions.map(formatObservedAction).join(', ')}`);
297
654
  }
298
- if (finishReason !== 'stop') {
299
- lines.push(`Le run s’est termine avec la raison: ${finishReason}.`);
655
+ if (blockingRequiredActions.length > 0) {
656
+ lines.push(`blocking_required_actions=${blockingRequiredActions.map((action) => `${action.title} [${action.kind}]`).join(', ')}`);
657
+ }
658
+ if (followUpRequiredActions.length > 0) {
659
+ lines.push(`follow_up_actions=${followUpRequiredActions.map((action) => `${action.title} [${action.kind}]`).join(', ')}`);
300
660
  }
301
661
  return lines.join('\n');
302
662
  }
303
- function ensureFinalText(prompt, finishReason, journal, existingText, requiredActions, completionAccepted) {
663
+ function buildCompletionRepairPrompt() {
664
+ return wrapInternal([
665
+ 'Yagr internal completion repair pass.',
666
+ 'The previous attempt ended without a concrete result or a structured blocker.',
667
+ 'Do not apologize and do not summarize a failure yet.',
668
+ 'Continue working until you either produce a real result or raise requestRequiredAction for the missing user input, permission, or external dependency that blocks progress.',
669
+ 'Do not treat follow-up runtime configuration or post-deploy setup as a blocking reason to stop if you can still build, validate, save, or deploy the current artifact.',
670
+ 'If the artifact can be delivered now but still needs later setup, prefer delivering it and raise requestRequiredAction with blocking=false for the next step.',
671
+ 'A plain-text statement that the task could not be completed is not an acceptable stopping point.',
672
+ ].join(' '));
673
+ }
674
+ function buildBlockerCapturePrompt() {
675
+ return wrapInternal([
676
+ 'Yagr internal blocker capture pass.',
677
+ 'The run still has no concrete result and no structured blocker.',
678
+ 'If progress is blocked on missing user input, permission, or an external dependency, call requestRequiredAction now.',
679
+ 'Do not answer with plain prose. Emit only the required action through the tool.',
680
+ ].join(' '));
681
+ }
682
+ async function ensureFinalText(prompt, finishReason, journal, existingText, requiredActions, completionAccepted, options, strategy) {
304
683
  const sanitizedText = sanitizeAssistantOutput(existingText);
305
- if (completionAccepted && sanitizedText) {
684
+ const forceGroundedFinalAnswer = shouldForceGroundedFinalAnswer(journal, requiredActions);
685
+ if (completionAccepted && !forceGroundedFinalAnswer && sanitizedText && !looksLikeRawToolIntentText(sanitizedText)) {
306
686
  return sanitizedText;
307
687
  }
688
+ const finalAnswerFacts = buildFinalAnswerFacts(finishReason, journal, requiredActions);
689
+ try {
690
+ const result = await generateText({
691
+ abortSignal: options.abortSignal,
692
+ model: createLanguageModel(options),
693
+ system: [
694
+ 'You are writing the final answer to the user after an agent run.',
695
+ 'Use only the grounded facts you are given.',
696
+ 'Do not restate requested features, plans, or design goals as if they were implemented unless the grounded facts explicitly confirm them.',
697
+ 'Do not mention internal prompts, phases, journals, or tool names such as n8nac, list, skills, validate, push, or verify unless the user explicitly asked for internals.',
698
+ 'If the workflow is ready, say so briefly and include the workflow URL if it is useful.',
699
+ 'If follow-up setup is still needed after delivery, present it briefly as a next step rather than as a blocker.',
700
+ 'Do not describe UI elements, cards, banners, embeds, diagrams, or presentation widgets.',
701
+ 'If the task is not complete, explain the real blocker briefly and concretely.',
702
+ 'Never invent success. Never mention unsupported details.',
703
+ 'Keep the answer concise and user-facing.',
704
+ ].join('\n'),
705
+ messages: [
706
+ {
707
+ role: 'user',
708
+ content: [
709
+ `Original user request (context only, not proof of completion):\n${prompt}`,
710
+ '',
711
+ `Grounded run facts:\n${finalAnswerFacts}`,
712
+ '',
713
+ 'Write the final user-facing answer now using only the grounded run facts.',
714
+ ].join('\n'),
715
+ },
716
+ ],
717
+ maxSteps: 1,
718
+ providerOptions: strategy.providerOptions,
719
+ });
720
+ const finalText = sanitizeAssistantOutput(result.text);
721
+ if (finalText
722
+ && !looksLikeRawToolIntentText(finalText)
723
+ && finalAnswerSatisfiesGroundedWorkflowFacts(finalText, journal)) {
724
+ return finalText;
725
+ }
726
+ }
727
+ catch {
728
+ // Fall through to the last-resort grounded summary.
729
+ }
308
730
  return buildGroundedSummary(prompt, finishReason, journal, requiredActions)
309
731
  || 'Run stopped before producing a grounded result.';
310
732
  }
@@ -394,7 +816,7 @@ function shouldAttemptRecovery(outcome, attemptNumber, requiredActions) {
394
816
  if (requiredActions.length > 0) {
395
817
  return false;
396
818
  }
397
- if (outcome.unresolvedFailedActions.length > 0) {
819
+ if (outcome.blockingUnresolvedFailedActions.length > 0) {
398
820
  return true;
399
821
  }
400
822
  if (outcome.hasWorkflowWrites && (!outcome.successfulValidate || !outcome.successfulPush)) {
@@ -412,11 +834,25 @@ function buildRecoveryPrompt(outcome, attemptNumber) {
412
834
  missingChecks.push('push');
413
835
  }
414
836
  if (outcome.hasWorkflowWrites && !outcome.successfulVerify) {
415
- missingChecks.push('verification distante');
837
+ missingChecks.push('remote verification');
838
+ }
839
+ // When the workflow file was already written but deployment steps are still
840
+ // missing and there are no failed actions, give a plain direct instruction
841
+ // instead of a confusing "inspect failing tool output" message. Some models
842
+ // (e.g. Gemini) do not respond to internal-tagged prompts in recovery passes
843
+ // when no concrete error is present.
844
+ if (outcome.hasWorkflowWrites && missingChecks.length > 0 && !failedActions) {
845
+ const writtenFile = outcome.writtenFiles.find((f) => f.endsWith('.workflow.ts')) ?? outcome.writtenFiles[0];
846
+ const fileNote = writtenFile ? ` (${writtenFile})` : '';
847
+ return [
848
+ `The workflow file${fileNote} was written successfully.`,
849
+ `The following deployment steps are still needed: ${missingChecks.join(', ')}.`,
850
+ 'Use the n8nac tool to complete them now. Do not stop until push succeeds.',
851
+ ].join(' ');
416
852
  }
417
853
  const issues = [
418
- failedActions ? `Actions en echec: ${failedActions}.` : '',
419
- missingChecks.length > 0 ? `Etapes non confirmees: ${missingChecks.join(', ')}.` : '',
854
+ failedActions ? `Failed actions: ${failedActions}.` : '',
855
+ missingChecks.length > 0 ? `Unconfirmed steps: ${missingChecks.join(', ')}.` : '',
420
856
  ].filter(Boolean).join(' ');
421
857
  return wrapInternal([
422
858
  `Yagr internal recovery pass ${attemptNumber}.`,
@@ -426,17 +862,63 @@ function buildRecoveryPrompt(outcome, attemptNumber) {
426
862
  'Only stop if a genuine blocker remains that cannot be resolved locally in this run.',
427
863
  ].join(' '));
428
864
  }
429
- async function executePhase(state, options, systemPrompt, tools, messages, maxSteps) {
865
+ /**
866
+ * Attempt to repair a malformed tool call where the model emitted an unquoted string value.
867
+ * This primarily handles `writeWorkspaceFile` when a model writes raw TypeScript code as the
868
+ * `content` field without JSON-string-encoding it. The closing `}` of the JSON object is always
869
+ * the last character in the args string, so `lastIndexOf('}')` reliably isolates it.
870
+ *
871
+ * Returns a corrected JSON args string, or `null` if the pattern is not recognised.
872
+ */
873
+ function tryRepairToolArgs(toolName, rawArgs) {
874
+ if (toolName !== 'writeWorkspaceFile')
875
+ return null;
876
+ const pathMatch = /"path"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(rawArgs);
877
+ if (!pathMatch)
878
+ return null;
879
+ const path = pathMatch[1];
880
+ const modeMatch = /"mode"\s*:\s*"(create|overwrite|append)"/.exec(rawArgs);
881
+ const mode = modeMatch?.[1] ?? 'overwrite';
882
+ const contentKeyIdx = rawArgs.indexOf('"content"');
883
+ if (contentKeyIdx === -1)
884
+ return null;
885
+ const colonIdx = rawArgs.indexOf(':', contentKeyIdx + 9);
886
+ if (colonIdx === -1)
887
+ return null;
888
+ let valueStart = colonIdx + 1;
889
+ while (valueStart < rawArgs.length && /[ \t]/.test(rawArgs[valueStart]))
890
+ valueStart++;
891
+ // If content is already a quoted JSON string the parse failure is a different issue
892
+ if (rawArgs[valueStart] === '"')
893
+ return null;
894
+ // The very last '}' in the string is the JSON object closing brace
895
+ const lastBrace = rawArgs.lastIndexOf('}');
896
+ if (lastBrace <= valueStart)
897
+ return null;
898
+ let rawContent = rawArgs.slice(valueStart, lastBrace);
899
+ // Strip a trailing mode field that the model may have appended after the raw content
900
+ rawContent = rawContent.replace(/,\s*"mode"\s*:\s*"(?:create|overwrite|append)"\s*$/, '').trim();
901
+ return JSON.stringify({ path, content: rawContent, mode });
902
+ }
903
+ async function executePhase(state, options, strategy, systemPrompt, tools, messages, maxSteps) {
430
904
  throwIfAborted(options.abortSignal);
431
- if (options.provider === 'mistral' || options.provider === 'copilot-proxy' || options.provider === 'openai-proxy') {
905
+ const modelInvocationTools = strategy.tooling.toolCallMode === 'disabled' ? undefined : tools;
906
+ const repairToolCall = async ({ toolCall, error }) => {
907
+ if (!InvalidToolArgumentsError.isInstance(error))
908
+ return null;
909
+ const repairedArgs = tryRepairToolArgs(toolCall.toolName, toolCall.args);
910
+ return repairedArgs !== null ? { ...toolCall, args: repairedArgs } : null;
911
+ };
912
+ if (strategy.executionMode === 'generate') {
432
913
  const result = await generateText({
433
914
  abortSignal: options.abortSignal,
434
915
  model: createLanguageModel(options),
435
916
  system: systemPrompt,
436
- tools,
917
+ ...(modelInvocationTools ? { tools: modelInvocationTools } : {}),
437
918
  messages,
438
919
  maxSteps,
439
- providerOptions: providerOptionsForRun(options.provider),
920
+ providerOptions: strategy.providerOptions,
921
+ experimental_repairToolCall: repairToolCall,
440
922
  });
441
923
  for (const step of result.steps) {
442
924
  await recordStep(state, options, {
@@ -454,9 +936,6 @@ async function executePhase(state, options, systemPrompt, tools, messages, maxSt
454
936
  });
455
937
  }
456
938
  const finalText = sanitizeAssistantOutput(result.text);
457
- if (finalText) {
458
- await options.onTextDelta?.(finalText);
459
- }
460
939
  return {
461
940
  text: finalText,
462
941
  finishReason: String(result.finishReason),
@@ -471,11 +950,12 @@ async function executePhase(state, options, systemPrompt, tools, messages, maxSt
471
950
  abortSignal: options.abortSignal,
472
951
  model: createLanguageModel(options),
473
952
  system: systemPrompt,
474
- tools,
953
+ ...(modelInvocationTools ? { tools: modelInvocationTools } : {}),
475
954
  messages,
476
955
  maxSteps,
477
- toolCallStreaming: shouldUseToolCallStreaming(options.provider),
478
- providerOptions: providerOptionsForRun(options.provider),
956
+ toolCallStreaming: strategy.toolCallStreaming,
957
+ providerOptions: strategy.providerOptions,
958
+ experimental_repairToolCall: repairToolCall,
479
959
  onStepFinish: async (stepResult) => {
480
960
  recordedSteps += 1;
481
961
  for (const toolCall of stepResult.toolCalls) {
@@ -524,9 +1004,6 @@ async function executePhase(state, options, systemPrompt, tools, messages, maxSt
524
1004
  result.toolCalls,
525
1005
  ]);
526
1006
  const finalText = sanitizeAssistantOutput(resolved[0]);
527
- if (finalText) {
528
- await options.onTextDelta?.(finalText);
529
- }
530
1007
  return {
531
1008
  text: finalText,
532
1009
  finishReason: String(resolved[1]),
@@ -535,12 +1012,6 @@ async function executePhase(state, options, systemPrompt, tools, messages, maxSt
535
1012
  responseMessages: response.messages,
536
1013
  };
537
1014
  }
538
- function shouldUseToolCallStreaming(provider) {
539
- if (provider === 'mistral') {
540
- return false;
541
- }
542
- return true;
543
- }
544
1015
  async function transitionPhase(state, options, phase, status, message) {
545
1016
  if (status === 'started') {
546
1017
  state.currentPhase = phase;
@@ -609,15 +1080,25 @@ export class YagrRunEngine {
609
1080
  role: 'user',
610
1081
  content: prompt,
611
1082
  };
612
- const runtimeHooks = [...createDefaultRuntimeHooks(), ...(options.runtimeHooks ?? [])];
1083
+ const resolvedModelConfig = resolveLanguageModelConfig(options);
1084
+ await getProviderPlugin(resolvedModelConfig.provider).metadata?.primeModelMetadata?.({
1085
+ model: resolvedModelConfig.model,
1086
+ apiKey: resolvedModelConfig.apiKey,
1087
+ baseUrl: resolvedModelConfig.baseUrl,
1088
+ });
1089
+ const runtimeStrategy = resolveToolRuntimeStrategy(resolvedModelConfig.provider, resolvedModelConfig.model);
1090
+ const runtimeHooks = [...createDefaultRuntimeHooksForStrategy(runtimeStrategy), ...(options.runtimeHooks ?? [])];
613
1091
  const baseTools = buildTools(this.engine, {
614
1092
  onToolEvent: withRuntimeToolEvents(state, options),
1093
+ }, {
1094
+ allowedToolNames: runtimeStrategy.tooling.availableToolNames,
615
1095
  });
616
1096
  const tools = wrapToolsWithRuntimeHooks(baseTools, runtimeHooks, () => ({
617
1097
  runId: state.runId,
618
1098
  phase: state.currentPhase,
619
1099
  state: state.currentAgentState,
620
1100
  }), options.satisfiedRequiredActionIds);
1101
+ const modelInvocationTools = runtimeStrategy.tooling.toolCallMode === 'disabled' ? undefined : tools;
621
1102
  const persistedMessages = [currentUserMessage];
622
1103
  let executionContext = [...this.history, currentUserMessage];
623
1104
  await emitJournal(state, options, {
@@ -633,16 +1114,22 @@ export class YagrRunEngine {
633
1114
  executionContext = await maybeCompactMessages(state, options, this.systemPrompt, prompt, executionContext);
634
1115
  const inspectInstruction = {
635
1116
  role: 'user',
636
- content: createPhasePrompt('inspect', prompt),
1117
+ content: createPhasePrompt('inspect', prompt, runtimeStrategy),
637
1118
  };
638
1119
  const inspectResult = await generateText({
639
1120
  abortSignal: options.abortSignal,
640
1121
  model: createLanguageModel(options),
641
1122
  system: this.systemPrompt,
642
- tools,
1123
+ ...(modelInvocationTools ? { tools: modelInvocationTools } : {}),
643
1124
  messages: [...executionContext, inspectInstruction],
644
- maxSteps: Math.min(options.maxSteps ?? 8, INSPECT_MAX_STEPS),
645
- providerOptions: providerOptionsForRun(options.provider),
1125
+ maxSteps: Math.min(options.maxSteps ?? 8, runtimeStrategy.inspectMaxSteps),
1126
+ providerOptions: runtimeStrategy.providerOptions,
1127
+ experimental_repairToolCall: async ({ toolCall, error }) => {
1128
+ if (!InvalidToolArgumentsError.isInstance(error))
1129
+ return null;
1130
+ const repairedArgs = tryRepairToolArgs(toolCall.toolName, toolCall.args);
1131
+ return repairedArgs !== null ? { ...toolCall, args: repairedArgs } : null;
1132
+ },
646
1133
  });
647
1134
  for (const step of inspectResult.steps) {
648
1135
  await recordStep(state, options, {
@@ -662,34 +1149,47 @@ export class YagrRunEngine {
662
1149
  if (state.currentPhase === 'inspect') {
663
1150
  await transitionPhase(state, options, 'inspect', 'completed', 'Inspection completed.');
664
1151
  }
1152
+ // Save base context before adding inspect history. The execute phase
1153
+ // starts from this clean base to avoid context-length issues that cause
1154
+ // some models (notably Gemini) to return empty completions when faced
1155
+ // with accumulated inspect tool-result messages.
1156
+ const executeBaseContext = [...executionContext];
665
1157
  executionContext.push(...sanitizeAssistantResponseMessages(inspectResult.response.messages));
666
1158
  throwIfAborted(options.abortSignal);
667
- await transitionPhase(state, options, 'plan', 'started', 'Preparing execution plan.');
668
- await transitionPhase(state, options, 'plan', 'completed', 'Execution plan ready.');
1159
+ if (getPhaseIndex(state.currentPhase) <= getPhaseIndex('plan')) {
1160
+ await transitionPhase(state, options, 'plan', 'started', 'Preparing execution plan.');
1161
+ await transitionPhase(state, options, 'plan', 'completed', 'Execution plan ready.');
1162
+ }
669
1163
  const executeInstruction = {
670
1164
  role: 'user',
671
- content: createPhasePrompt('execute', prompt),
1165
+ content: createPhasePrompt('execute', prompt, runtimeStrategy),
672
1166
  };
673
- let executeMessages = [...executionContext, executeInstruction];
1167
+ let executeMessages = [...executeBaseContext, executeInstruction];
674
1168
  let text = '';
675
1169
  let finishReason = 'stop';
676
1170
  let steps = 0;
677
1171
  let toolCalls = [];
678
1172
  let responseMessages = [];
1173
+ let lastExecutionResponseMessages = [];
1174
+ let blockerCaptureAttempts = 0;
679
1175
  for (let attemptNumber = 1; attemptNumber <= MAX_EXECUTION_ATTEMPTS; attemptNumber += 1) {
680
1176
  throwIfAborted(options.abortSignal);
681
1177
  executeMessages = await maybeCompactMessages(state, options, this.systemPrompt, prompt, executeMessages);
682
- const phaseResult = await executePhase(state, options, this.systemPrompt, tools, executeMessages, attemptNumber === 1 ? (options.maxSteps ?? EXECUTE_MAX_STEPS) : Math.min(options.maxSteps ?? EXECUTE_MAX_STEPS, EXECUTE_RECOVERY_MAX_STEPS));
1178
+ const phaseResult = await executePhase(state, options, runtimeStrategy, this.systemPrompt, tools, executeMessages, attemptNumber === 1
1179
+ ? (options.maxSteps ?? runtimeStrategy.executeMaxSteps)
1180
+ : Math.min(options.maxSteps ?? runtimeStrategy.executeMaxSteps, runtimeStrategy.recoveryMaxSteps));
683
1181
  if (phaseResult.text) {
684
1182
  text = phaseResult.text;
685
1183
  }
686
1184
  finishReason = phaseResult.finishReason;
687
1185
  steps += phaseResult.steps;
688
1186
  toolCalls = phaseResult.toolCalls;
689
- responseMessages = [...responseMessages, ...sanitizeAssistantResponseMessages(phaseResult.responseMessages)];
1187
+ lastExecutionResponseMessages = sanitizeAssistantResponseMessages(phaseResult.responseMessages);
1188
+ responseMessages = [...responseMessages, ...lastExecutionResponseMessages];
1189
+ const executedSyntheticIntents = await maybeExecuteSyntheticToolIntents(state, options, runtimeStrategy, tools, phaseResult);
690
1190
  const outcome = analyzeRunOutcome(state.journal);
691
1191
  const requiredActions = collectRequiredActions(state.journal);
692
- if (!shouldAttemptRecovery(outcome, attemptNumber, requiredActions)) {
1192
+ if (!shouldAttemptRecovery(outcome, attemptNumber, requiredActions) || executedSyntheticIntents) {
693
1193
  break;
694
1194
  }
695
1195
  throwIfAborted(options.abortSignal);
@@ -699,31 +1199,129 @@ export class YagrRunEngine {
699
1199
  status: 'started',
700
1200
  message: `Recovery pass ${attemptNumber + 1} triggered after failed or incomplete execution steps.`,
701
1201
  });
702
- executeMessages = [
703
- ...executeMessages,
704
- ...sanitizeAssistantResponseMessages(phaseResult.responseMessages),
705
- {
706
- role: 'user',
707
- content: buildRecoveryPrompt(outcome, attemptNumber + 1),
708
- },
709
- ];
1202
+ // If the model's response was completely absent or stripped (e.g. empty
1203
+ // stop), appending the recovery prompt to the existing execute history
1204
+ // creates a long conversation that some models (e.g. Gemini) refuse to
1205
+ // continue. Instead, restart from the clean base context so the
1206
+ // recovery prompt is the only new instruction the model sees. This
1207
+ // mirrors the short context that the inspect phase uses, which those
1208
+ // models always respond to.
1209
+ const sanitizedPriorResponse = sanitizeAssistantResponseMessages(phaseResult.responseMessages);
1210
+ executeMessages = sanitizedPriorResponse.length === 0
1211
+ ? [
1212
+ ...executeBaseContext,
1213
+ {
1214
+ role: 'user',
1215
+ content: buildRecoveryPrompt(outcome, attemptNumber + 1),
1216
+ },
1217
+ ]
1218
+ : [
1219
+ ...executeMessages,
1220
+ ...sanitizedPriorResponse,
1221
+ {
1222
+ role: 'user',
1223
+ content: buildRecoveryPrompt(outcome, attemptNumber + 1),
1224
+ },
1225
+ ];
710
1226
  }
711
1227
  persistedMessages.push(...responseMessages);
712
1228
  steps += inspectResult.steps.length;
713
1229
  throwIfAborted(options.abortSignal);
714
- const requiredActions = collectRequiredActions(state.journal);
715
- const completionDecision = await evaluateCompletionGate({
716
- text,
717
- finishReason,
718
- requiredActions,
719
- satisfiedRequiredActionIds: options.satisfiedRequiredActionIds,
720
- hasWorkflowWrites: analyzeRunOutcome(state.journal).hasWorkflowWrites,
721
- successfulValidate: Boolean(analyzeRunOutcome(state.journal).successfulValidate),
722
- successfulPush: Boolean(analyzeRunOutcome(state.journal).successfulPush),
723
- unresolvedFailureCount: analyzeRunOutcome(state.journal).unresolvedFailedActions.length,
724
- hooks: runtimeHooks,
725
- context: buildRuntimeContext(state),
726
- });
1230
+ let completionRepairAttempts = 0;
1231
+ let completionDecision;
1232
+ let finalOutcome;
1233
+ let requiredActions;
1234
+ for (;;) {
1235
+ requiredActions = collectRequiredActions(state.journal);
1236
+ finalOutcome = analyzeRunOutcome(state.journal);
1237
+ completionDecision = await evaluateCompletionGate({
1238
+ text,
1239
+ finishReason,
1240
+ requiredActions,
1241
+ satisfiedRequiredActionIds: options.satisfiedRequiredActionIds,
1242
+ attemptedMaterialWork: hasObservedToolCall(state.journal, runtimeStrategy.tooling.executionCriticalToolNames),
1243
+ hasConcreteResult: Boolean(finalOutcome.hasWorkflowWrites
1244
+ || finalOutcome.successfulValidate
1245
+ || finalOutcome.successfulPush
1246
+ || finalOutcome.successfulVerify
1247
+ || extractPresentedWorkflowFromJournal(state.journal)),
1248
+ hasWorkflowWrites: finalOutcome.hasWorkflowWrites,
1249
+ successfulValidate: Boolean(finalOutcome.successfulValidate),
1250
+ successfulPush: Boolean(finalOutcome.successfulPush),
1251
+ successfulVerify: Boolean(finalOutcome.successfulVerify),
1252
+ unresolvedFailureCount: finalOutcome.blockingUnresolvedFailedActions.length,
1253
+ hooks: runtimeHooks,
1254
+ context: buildRuntimeContext(state),
1255
+ });
1256
+ if (!completionDecision.accepted
1257
+ && completionDecision.needsContinuation
1258
+ && completionRepairAttempts < MAX_COMPLETION_REPAIR_ATTEMPTS) {
1259
+ completionRepairAttempts += 1;
1260
+ throwIfAborted(options.abortSignal);
1261
+ executeMessages = await maybeCompactMessages(state, options, this.systemPrompt, prompt, executeMessages);
1262
+ executeMessages = [
1263
+ ...executeMessages,
1264
+ ...lastExecutionResponseMessages,
1265
+ {
1266
+ role: 'user',
1267
+ content: buildCompletionRepairPrompt(),
1268
+ },
1269
+ ];
1270
+ const phaseResult = await executePhase(state, options, runtimeStrategy, this.systemPrompt, tools, executeMessages, Math.min(options.maxSteps ?? runtimeStrategy.executeMaxSteps, runtimeStrategy.recoveryMaxSteps));
1271
+ if (phaseResult.text) {
1272
+ text = phaseResult.text;
1273
+ }
1274
+ finishReason = phaseResult.finishReason;
1275
+ steps += phaseResult.steps;
1276
+ toolCalls = phaseResult.toolCalls;
1277
+ lastExecutionResponseMessages = sanitizeAssistantResponseMessages(phaseResult.responseMessages);
1278
+ responseMessages = [...responseMessages, ...lastExecutionResponseMessages];
1279
+ persistedMessages.push(...lastExecutionResponseMessages);
1280
+ await maybeExecuteSyntheticToolIntents(state, options, runtimeStrategy, tools, phaseResult);
1281
+ continue;
1282
+ }
1283
+ if (!completionDecision.accepted
1284
+ && completionDecision.needsContinuation
1285
+ && completionDecision.requiredActions.length === 0
1286
+ && blockerCaptureAttempts < MAX_BLOCKER_CAPTURE_ATTEMPTS) {
1287
+ blockerCaptureAttempts += 1;
1288
+ throwIfAborted(options.abortSignal);
1289
+ executeMessages = await maybeCompactMessages(state, options, this.systemPrompt, prompt, executeMessages);
1290
+ executeMessages = [
1291
+ ...executeMessages,
1292
+ ...lastExecutionResponseMessages,
1293
+ {
1294
+ role: 'user',
1295
+ content: buildBlockerCapturePrompt(),
1296
+ },
1297
+ ];
1298
+ const blockerTools = {
1299
+ requestRequiredAction: tools.requestRequiredAction,
1300
+ };
1301
+ const phaseResult = await executePhase(state, options, runtimeStrategy, this.systemPrompt, blockerTools, executeMessages, 1);
1302
+ if (phaseResult.text) {
1303
+ text = phaseResult.text;
1304
+ }
1305
+ finishReason = phaseResult.finishReason;
1306
+ steps += phaseResult.steps;
1307
+ toolCalls = phaseResult.toolCalls;
1308
+ lastExecutionResponseMessages = sanitizeAssistantResponseMessages(phaseResult.responseMessages);
1309
+ responseMessages = [...responseMessages, ...lastExecutionResponseMessages];
1310
+ persistedMessages.push(...lastExecutionResponseMessages);
1311
+ continue;
1312
+ }
1313
+ break;
1314
+ }
1315
+ if (!completionDecision.accepted
1316
+ && completionDecision.needsContinuation
1317
+ && completionDecision.requiredActions.length === 0) {
1318
+ completionDecision = {
1319
+ ...completionDecision,
1320
+ state: 'failed_terminal',
1321
+ reasons: ['Run ended without a concrete result and without a structured blocker.'],
1322
+ needsContinuation: false,
1323
+ };
1324
+ }
727
1325
  for (const requiredAction of completionDecision.requiredActions) {
728
1326
  await emitJournal(state, options, {
729
1327
  type: 'state',
@@ -735,7 +1333,11 @@ export class YagrRunEngine {
735
1333
  });
736
1334
  }
737
1335
  throwIfAborted(options.abortSignal);
738
- text = ensureFinalText(prompt, finishReason, state.journal, text, completionDecision.requiredActions, completionDecision.accepted);
1336
+ await maybeEmitSyntheticWorkflowEmbed(finalOutcome, state.journal, options.onToolEvent);
1337
+ text = await ensureFinalText(prompt, finishReason, state.journal, text, completionDecision.requiredActions, completionDecision.accepted, options, runtimeStrategy);
1338
+ if (text) {
1339
+ await options.onTextDelta?.(text);
1340
+ }
739
1341
  if (state.currentPhase && state.currentPhase !== 'summarize') {
740
1342
  await transitionPhase(state, options, state.currentPhase, 'completed', `${state.currentPhase} phase completed.`);
741
1343
  }