@vybestack/llxprt-code-core 0.4.8 → 0.5.0-nightly.251102.e5b51aa3

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 (213) hide show
  1. package/dist/prompt-config/defaults/default-prompts.json +4 -17
  2. package/dist/src/auth/precedence.d.ts +69 -9
  3. package/dist/src/auth/precedence.js +467 -69
  4. package/dist/src/auth/precedence.js.map +1 -1
  5. package/dist/src/auth/types.d.ts +2 -2
  6. package/dist/src/config/config.d.ts +15 -1
  7. package/dist/src/config/config.js +118 -6
  8. package/dist/src/config/config.js.map +1 -1
  9. package/dist/src/config/index.d.ts +6 -0
  10. package/dist/src/config/index.js +5 -0
  11. package/dist/src/config/index.js.map +1 -1
  12. package/dist/src/config/profileManager.d.ts +23 -3
  13. package/dist/src/config/profileManager.js +54 -7
  14. package/dist/src/config/profileManager.js.map +1 -1
  15. package/dist/src/config/subagentManager.d.ts +96 -0
  16. package/dist/src/config/subagentManager.js +371 -0
  17. package/dist/src/config/subagentManager.js.map +1 -0
  18. package/dist/src/config/types.d.ts +18 -0
  19. package/dist/src/config/types.js +3 -0
  20. package/dist/src/config/types.js.map +1 -0
  21. package/dist/src/core/client.d.ts +27 -7
  22. package/dist/src/core/client.js +231 -55
  23. package/dist/src/core/client.js.map +1 -1
  24. package/dist/src/core/contentGenerator.d.ts +3 -1
  25. package/dist/src/core/contentGenerator.js +3 -0
  26. package/dist/src/core/contentGenerator.js.map +1 -1
  27. package/dist/src/core/coreToolScheduler.d.ts +1 -5
  28. package/dist/src/core/coreToolScheduler.js +95 -23
  29. package/dist/src/core/coreToolScheduler.js.map +1 -1
  30. package/dist/src/core/geminiChat.d.ts +42 -12
  31. package/dist/src/core/geminiChat.js +405 -205
  32. package/dist/src/core/geminiChat.js.map +1 -1
  33. package/dist/src/core/nonInteractiveToolExecutor.d.ts +3 -2
  34. package/dist/src/core/nonInteractiveToolExecutor.js +94 -10
  35. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  36. package/dist/src/core/subagent.d.ts +86 -7
  37. package/dist/src/core/subagent.js +809 -79
  38. package/dist/src/core/subagent.js.map +1 -1
  39. package/dist/src/core/subagentOrchestrator.d.ts +73 -0
  40. package/dist/src/core/subagentOrchestrator.js +387 -0
  41. package/dist/src/core/subagentOrchestrator.js.map +1 -0
  42. package/dist/src/core/subagentScheduler.d.ts +16 -0
  43. package/dist/src/core/subagentScheduler.js +7 -0
  44. package/dist/src/core/subagentScheduler.js.map +1 -0
  45. package/dist/src/core/turn.d.ts +5 -1
  46. package/dist/src/core/turn.js +5 -1
  47. package/dist/src/core/turn.js.map +1 -1
  48. package/dist/src/hooks/tool-render-suppression-hook.js +6 -1
  49. package/dist/src/hooks/tool-render-suppression-hook.js.map +1 -1
  50. package/dist/src/ide/ideContext.d.ts +32 -32
  51. package/dist/src/index.d.ts +19 -1
  52. package/dist/src/index.js +15 -2
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/interfaces/index.d.ts +1 -0
  55. package/dist/src/interfaces/index.js +4 -0
  56. package/dist/src/interfaces/index.js.map +1 -0
  57. package/dist/src/interfaces/nodejs-error.interface.d.ts +4 -0
  58. package/dist/src/interfaces/nodejs-error.interface.js +2 -0
  59. package/dist/src/interfaces/nodejs-error.interface.js.map +1 -0
  60. package/dist/src/parsers/TextToolCallParser.d.ts +17 -1
  61. package/dist/src/parsers/TextToolCallParser.js +542 -148
  62. package/dist/src/parsers/TextToolCallParser.js.map +1 -1
  63. package/dist/src/prompt-config/defaults/core.md +15 -0
  64. package/dist/src/prompt-config/defaults/providers/gemini/core.md +203 -119
  65. package/dist/src/prompt-config/defaults/tool-defaults.js +2 -0
  66. package/dist/src/prompt-config/defaults/tool-defaults.js.map +1 -1
  67. package/dist/src/prompt-config/defaults/tools/list-subagents.md +7 -0
  68. package/dist/src/prompt-config/defaults/tools/task.md +8 -0
  69. package/dist/src/providers/BaseProvider.d.ts +115 -30
  70. package/dist/src/providers/BaseProvider.js +445 -109
  71. package/dist/src/providers/BaseProvider.js.map +1 -1
  72. package/dist/src/providers/IProvider.d.ts +50 -18
  73. package/dist/src/providers/LoggingProviderWrapper.d.ts +60 -16
  74. package/dist/src/providers/LoggingProviderWrapper.js +213 -60
  75. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  76. package/dist/src/providers/ProviderManager.d.ts +73 -2
  77. package/dist/src/providers/ProviderManager.js +492 -40
  78. package/dist/src/providers/ProviderManager.js.map +1 -1
  79. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +35 -38
  80. package/dist/src/providers/anthropic/AnthropicProvider.js +222 -227
  81. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  82. package/dist/src/providers/errors.d.ts +86 -0
  83. package/dist/src/providers/errors.js +89 -0
  84. package/dist/src/providers/errors.js.map +1 -1
  85. package/dist/src/providers/gemini/GeminiProvider.d.ts +101 -41
  86. package/dist/src/providers/gemini/GeminiProvider.js +386 -311
  87. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  88. package/dist/src/providers/openai/ConversationCache.d.ts +5 -3
  89. package/dist/src/providers/openai/ConversationCache.js +93 -32
  90. package/dist/src/providers/openai/ConversationCache.js.map +1 -1
  91. package/dist/src/providers/openai/OpenAIProvider.d.ts +82 -42
  92. package/dist/src/providers/openai/OpenAIProvider.js +391 -536
  93. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  94. package/dist/src/providers/openai/getOpenAIProviderInfo.d.ts +1 -1
  95. package/dist/src/providers/openai/getOpenAIProviderInfo.js +52 -22
  96. package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
  97. package/dist/src/providers/openai/openaiRequestParams.d.ts +7 -0
  98. package/dist/src/providers/openai/openaiRequestParams.js +66 -0
  99. package/dist/src/providers/openai/openaiRequestParams.js.map +1 -0
  100. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +6 -33
  101. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +84 -183
  102. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
  103. package/dist/src/providers/types/providerRuntime.d.ts +17 -0
  104. package/dist/src/providers/types/providerRuntime.js +7 -0
  105. package/dist/src/providers/types/providerRuntime.js.map +1 -0
  106. package/dist/src/providers/utils/authToken.d.ts +12 -0
  107. package/dist/src/providers/utils/authToken.js +17 -0
  108. package/dist/src/providers/utils/authToken.js.map +1 -0
  109. package/dist/src/providers/utils/userMemory.d.ts +8 -0
  110. package/dist/src/providers/utils/userMemory.js +34 -0
  111. package/dist/src/providers/utils/userMemory.js.map +1 -0
  112. package/dist/src/runtime/AgentRuntimeContext.d.ts +213 -0
  113. package/dist/src/runtime/AgentRuntimeContext.js +17 -0
  114. package/dist/src/runtime/AgentRuntimeContext.js.map +1 -0
  115. package/dist/src/runtime/AgentRuntimeLoader.d.ts +47 -0
  116. package/dist/src/runtime/AgentRuntimeLoader.js +122 -0
  117. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -0
  118. package/dist/src/runtime/AgentRuntimeState.d.ts +232 -0
  119. package/dist/src/runtime/AgentRuntimeState.js +439 -0
  120. package/dist/src/runtime/AgentRuntimeState.js.map +1 -0
  121. package/dist/src/runtime/RuntimeInvocationContext.d.ts +51 -0
  122. package/dist/src/runtime/RuntimeInvocationContext.js +52 -0
  123. package/dist/src/runtime/RuntimeInvocationContext.js.map +1 -0
  124. package/dist/src/runtime/createAgentRuntimeContext.d.ts +7 -0
  125. package/dist/src/runtime/createAgentRuntimeContext.js +65 -0
  126. package/dist/src/runtime/createAgentRuntimeContext.js.map +1 -0
  127. package/dist/src/runtime/index.d.ts +13 -0
  128. package/dist/src/runtime/index.js +14 -0
  129. package/dist/src/runtime/index.js.map +1 -0
  130. package/dist/src/runtime/providerRuntimeContext.d.ts +30 -0
  131. package/dist/src/runtime/providerRuntimeContext.js +70 -0
  132. package/dist/src/runtime/providerRuntimeContext.js.map +1 -0
  133. package/dist/src/runtime/runtimeAdapters.d.ts +22 -0
  134. package/dist/src/runtime/runtimeAdapters.js +81 -0
  135. package/dist/src/runtime/runtimeAdapters.js.map +1 -0
  136. package/dist/src/runtime/runtimeStateFactory.d.ts +21 -0
  137. package/dist/src/runtime/runtimeStateFactory.js +104 -0
  138. package/dist/src/runtime/runtimeStateFactory.js.map +1 -0
  139. package/dist/src/services/todo-context-tracker.d.ts +10 -8
  140. package/dist/src/services/todo-context-tracker.js +26 -10
  141. package/dist/src/services/todo-context-tracker.js.map +1 -1
  142. package/dist/src/services/tool-call-tracker-service.d.ts +11 -7
  143. package/dist/src/services/tool-call-tracker-service.js +89 -29
  144. package/dist/src/services/tool-call-tracker-service.js.map +1 -1
  145. package/dist/src/settings/SettingsService.d.ts +4 -0
  146. package/dist/src/settings/SettingsService.js +65 -2
  147. package/dist/src/settings/SettingsService.js.map +1 -1
  148. package/dist/src/settings/settingsServiceInstance.d.ts +6 -1
  149. package/dist/src/settings/settingsServiceInstance.js +28 -8
  150. package/dist/src/settings/settingsServiceInstance.js.map +1 -1
  151. package/dist/src/telemetry/loggers.d.ts +5 -1
  152. package/dist/src/telemetry/loggers.js.map +1 -1
  153. package/dist/src/telemetry/loggers.test.circular.js +4 -0
  154. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  155. package/dist/src/telemetry/metrics.d.ts +3 -1
  156. package/dist/src/telemetry/metrics.js.map +1 -1
  157. package/dist/src/telemetry/types.d.ts +1 -0
  158. package/dist/src/telemetry/types.js +3 -0
  159. package/dist/src/telemetry/types.js.map +1 -1
  160. package/dist/src/test-utils/index.d.ts +2 -0
  161. package/dist/src/test-utils/index.js +2 -0
  162. package/dist/src/test-utils/index.js.map +1 -1
  163. package/dist/src/test-utils/mockWorkspaceContext.d.ts +0 -3
  164. package/dist/src/test-utils/mockWorkspaceContext.js +3 -4
  165. package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -1
  166. package/dist/src/test-utils/providerCallOptions.d.ts +43 -0
  167. package/dist/src/test-utils/providerCallOptions.js +137 -0
  168. package/dist/src/test-utils/providerCallOptions.js.map +1 -0
  169. package/dist/src/test-utils/runtime.d.ts +92 -0
  170. package/dist/src/test-utils/runtime.js +226 -0
  171. package/dist/src/test-utils/runtime.js.map +1 -0
  172. package/dist/src/test-utils/tools.d.ts +4 -4
  173. package/dist/src/test-utils/tools.js +20 -10
  174. package/dist/src/test-utils/tools.js.map +1 -1
  175. package/dist/src/tools/list-subagents.d.ts +31 -0
  176. package/dist/src/tools/list-subagents.js +109 -0
  177. package/dist/src/tools/list-subagents.js.map +1 -0
  178. package/dist/src/tools/task.d.ts +87 -0
  179. package/dist/src/tools/task.js +427 -0
  180. package/dist/src/tools/task.js.map +1 -0
  181. package/dist/src/tools/todo-read.js +1 -1
  182. package/dist/src/tools/todo-read.js.map +1 -1
  183. package/dist/src/tools/todo-store.js +4 -2
  184. package/dist/src/tools/todo-store.js.map +1 -1
  185. package/dist/src/tools/todo-write.js +4 -2
  186. package/dist/src/tools/todo-write.js.map +1 -1
  187. package/dist/src/tools/tool-error.d.ts +1 -0
  188. package/dist/src/tools/tool-error.js +1 -0
  189. package/dist/src/tools/tool-error.js.map +1 -1
  190. package/dist/src/tools/tool-registry.d.ts +2 -0
  191. package/dist/src/tools/tool-registry.js +46 -21
  192. package/dist/src/tools/tool-registry.js.map +1 -1
  193. package/dist/src/types/modelParams.d.ts +4 -0
  194. package/dist/src/utils/editor.js +10 -8
  195. package/dist/src/utils/editor.js.map +1 -1
  196. package/dist/src/utils/gitIgnoreParser.js +15 -3
  197. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  198. package/package.json +1 -1
  199. package/dist/src/prompt-config/defaults/providers/anthropic/core.md +0 -97
  200. package/dist/src/prompt-config/defaults/providers/anthropic/tools/glob.md +0 -34
  201. package/dist/src/prompt-config/defaults/providers/anthropic/tools/list-directory.md +0 -11
  202. package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-file.md +0 -14
  203. package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-many-files.md +0 -31
  204. package/dist/src/prompt-config/defaults/providers/anthropic/tools/replace.md +0 -41
  205. package/dist/src/prompt-config/defaults/providers/anthropic/tools/run-shell-command.md +0 -32
  206. package/dist/src/prompt-config/defaults/providers/anthropic/tools/save-memory.md +0 -35
  207. package/dist/src/prompt-config/defaults/providers/anthropic/tools/search-file-content.md +0 -44
  208. package/dist/src/prompt-config/defaults/providers/anthropic/tools/todo-write.md +0 -45
  209. package/dist/src/prompt-config/defaults/providers/anthropic/tools/write-file.md +0 -11
  210. package/dist/src/prompt-config/defaults/providers/openai/core.md +0 -97
  211. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-pause.md +0 -28
  212. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-read.md +0 -5
  213. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-write.md +0 -45
@@ -3,12 +3,22 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ /**
7
+ * @plan PLAN-20251028-STATELESS6.P08
8
+ * @requirement REQ-STAT6-001.1, REQ-STAT6-003.1
9
+ * @pseudocode agent-runtime-context.md lines 92-101
10
+ */
6
11
  import { reportError } from '../utils/errorReporting.js';
7
- import { executeToolCall } from './nonInteractiveToolExecutor.js';
8
- import { createContentGenerator } from './contentGenerator.js';
9
- import { getEnvironmentContext } from '../utils/environmentContext.js';
12
+ import { DebugLogger } from '../debug/DebugLogger.js';
13
+ import { ApprovalMode } from '../config/config.js';
14
+ import { GeminiEventType, Turn } from './turn.js';
15
+ import { executeToolCall, } from './nonInteractiveToolExecutor.js';
10
16
  import { Type, } from '@google/genai';
11
17
  import { GeminiChat, StreamEventType } from './geminiChat.js';
18
+ import { GemmaToolCallParser } from '../parsers/TextToolCallParser.js';
19
+ import { TodoStore } from '../tools/todo-store.js';
20
+ import { CoreToolScheduler, } from './coreToolScheduler.js';
21
+ import { ToolErrorType } from '../tools/tool-error.js';
12
22
  /**
13
23
  * @fileoverview Defines the configuration interfaces for a subagent.
14
24
  *
@@ -38,6 +48,7 @@ export var SubagentTerminateMode;
38
48
  */
39
49
  SubagentTerminateMode["MAX_TURNS"] = "MAX_TURNS";
40
50
  })(SubagentTerminateMode || (SubagentTerminateMode = {}));
51
+ const defaultEnvironmentContextLoader = async () => [];
41
52
  /**
42
53
  * Manages the runtime context state for the subagent.
43
54
  * This class provides a mechanism to store and retrieve key-value pairs
@@ -73,6 +84,86 @@ export class ContextState {
73
84
  return Object.keys(this.state);
74
85
  }
75
86
  }
87
+ const normalizeToolName = (name) => name.trim().toLowerCase();
88
+ function convertMetadataToFunctionDeclaration(fallbackName, metadata) {
89
+ const rawSchema = metadata.parameterSchema && typeof metadata.parameterSchema === 'object'
90
+ ? { ...metadata.parameterSchema }
91
+ : {};
92
+ const properties = rawSchema.properties ?? {};
93
+ return {
94
+ name: metadata.name ?? fallbackName,
95
+ description: metadata.description ?? '',
96
+ parameters: {
97
+ ...rawSchema,
98
+ type: rawSchema.type ?? Type.OBJECT,
99
+ properties,
100
+ },
101
+ };
102
+ }
103
+ async function validateToolsAgainstRuntime(params) {
104
+ const { toolConfig, toolRegistry, toolsView } = params;
105
+ const allowedNames = new Set((typeof toolsView.listToolNames === 'function'
106
+ ? toolsView.listToolNames()
107
+ : []).map(normalizeToolName));
108
+ for (const toolEntry of toolConfig.tools) {
109
+ if (typeof toolEntry !== 'string') {
110
+ continue;
111
+ }
112
+ if (allowedNames.size > 0 &&
113
+ !allowedNames.has(normalizeToolName(toolEntry))) {
114
+ throw new Error(`Tool "${toolEntry}" is not permitted for this runtime bundle.`);
115
+ }
116
+ const tool = toolRegistry.getTool(toolEntry);
117
+ if (!tool) {
118
+ continue;
119
+ }
120
+ }
121
+ }
122
+ function createToolExecutionConfig(runtimeBundle, toolRegistry, settingsSnapshot, toolConfig) {
123
+ const ephemerals = buildEphemeralSettings(settingsSnapshot);
124
+ if (toolConfig && Array.isArray(toolConfig.tools)) {
125
+ const normalizedWhitelist = toolConfig.tools
126
+ .filter((entry) => typeof entry === 'string')
127
+ .map((entry) => entry.trim())
128
+ .filter((entry) => entry.length > 0)
129
+ .map((entry) => entry.toLowerCase());
130
+ if (normalizedWhitelist.length > 0) {
131
+ const existingAllowed = Array.isArray(ephemerals['tools.allowed'])
132
+ ? ephemerals['tools.allowed']
133
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
134
+ .filter((entry) => entry.length > 0)
135
+ .map((entry) => entry.toLowerCase())
136
+ : [];
137
+ const allowedSet = existingAllowed.length > 0
138
+ ? normalizedWhitelist.filter((entry) => existingAllowed.includes(entry))
139
+ : normalizedWhitelist;
140
+ ephemerals['tools.allowed'] = Array.from(new Set(allowedSet));
141
+ }
142
+ }
143
+ return {
144
+ getToolRegistry: () => toolRegistry,
145
+ getEphemeralSettings: () => ({ ...ephemerals }),
146
+ getEphemeralSetting: (key) => ephemerals[key],
147
+ getExcludeTools: () => [],
148
+ getSessionId: () => runtimeBundle.runtimeContext.state.sessionId,
149
+ getTelemetryLogPromptsEnabled: () => Boolean(settingsSnapshot?.telemetry?.enabled),
150
+ };
151
+ }
152
+ function buildEphemeralSettings(snapshot) {
153
+ const ephemerals = {
154
+ emojifilter: 'auto',
155
+ };
156
+ if (!snapshot) {
157
+ return ephemerals;
158
+ }
159
+ if (snapshot.tools?.allowed) {
160
+ ephemerals['tools.allowed'] = [...snapshot.tools.allowed];
161
+ }
162
+ if (snapshot.tools?.disabled) {
163
+ ephemerals['tools.disabled'] = [...snapshot.tools.disabled];
164
+ }
165
+ return ephemerals;
166
+ }
76
167
  /**
77
168
  * Replaces `${...}` placeholders in a template string with values from a context.
78
169
  *
@@ -101,13 +192,21 @@ function templateString(template, context) {
101
192
  * Represents the scope and execution environment for a subagent.
102
193
  * This class orchestrates the subagent's lifecycle, managing its chat interactions,
103
194
  * runtime context, and the collection of its outputs.
195
+ *
196
+ * @plan PLAN-20251028-STATELESS6.P08
197
+ * @requirement REQ-STAT6-001.1, REQ-STAT6-001.2, REQ-STAT6-003.1, REQ-STAT6-003.2
198
+ * @pseudocode agent-runtime-context.md line 93 (step 007.1)
104
199
  */
105
200
  export class SubAgentScope {
106
201
  name;
107
202
  runtimeContext;
108
- promptConfig;
109
203
  modelConfig;
110
204
  runConfig;
205
+ promptConfig;
206
+ contentGenerator;
207
+ toolExecutorContext;
208
+ environmentContextLoader;
209
+ config;
111
210
  toolConfig;
112
211
  outputConfig;
113
212
  output = {
@@ -115,80 +214,318 @@ export class SubAgentScope {
115
214
  emitted_vars: {},
116
215
  };
117
216
  subagentId;
217
+ logger = new DebugLogger('llxprt:subagent');
218
+ textToolParser = new GemmaToolCallParser();
219
+ activeAbortController = null;
118
220
  /** Optional callback for streaming text messages during execution */
119
221
  onMessage;
120
222
  /**
121
223
  * Constructs a new SubAgentScope instance.
224
+ *
225
+ * @plan PLAN-20251028-STATELESS6.P08
226
+ * @requirement REQ-STAT6-001.1
227
+ * @pseudocode agent-runtime-context.md line 93 (step 007.1)
228
+ *
122
229
  * @param name - The name for the subagent, used for logging and identification.
123
- * @param runtimeContext - The shared runtime configuration and services.
124
- * @param promptConfig - Configuration for the subagent's prompt and behavior.
230
+ * @param runtimeContext - Immutable runtime context (replaces Config parameter).
125
231
  * @param modelConfig - Configuration for the generative model parameters.
126
232
  * @param runConfig - Configuration for the subagent's execution environment.
233
+ * @param promptConfig - Configuration for the subagent's prompt and behavior.
234
+ * @param contentGenerator - Pre-initialized content generator for this subagent.
235
+ * @param toolRegistry - Active tool registry for execution and validation.
236
+ * @param toolExecutorContext - Stateless execution context used for tool invocations.
237
+ * @param environmentContextLoader - Function that resolves environment context for prompts.
127
238
  * @param toolConfig - Optional configuration for tools available to the subagent.
128
239
  * @param outputConfig - Optional configuration for the subagent's expected outputs.
129
240
  */
130
- constructor(name, runtimeContext, promptConfig, modelConfig, runConfig, toolConfig, outputConfig) {
241
+ constructor(name, runtimeContext, modelConfig, runConfig, promptConfig, contentGenerator, toolExecutorContext, environmentContextLoader, config, toolConfig, outputConfig) {
131
242
  this.name = name;
132
243
  this.runtimeContext = runtimeContext;
133
- this.promptConfig = promptConfig;
134
244
  this.modelConfig = modelConfig;
135
245
  this.runConfig = runConfig;
246
+ this.promptConfig = promptConfig;
247
+ this.contentGenerator = contentGenerator;
248
+ this.toolExecutorContext = toolExecutorContext;
249
+ this.environmentContextLoader = environmentContextLoader;
250
+ this.config = config;
136
251
  this.toolConfig = toolConfig;
137
252
  this.outputConfig = outputConfig;
138
253
  const randomPart = Math.random().toString(36).slice(2, 8);
139
254
  this.subagentId = `${this.name}-${randomPart}`;
140
255
  }
256
+ /**
257
+ * Returns the unique agent identifier assigned to this subagent scope.
258
+ */
259
+ getAgentId() {
260
+ return this.subagentId;
261
+ }
141
262
  /**
142
263
  * Creates and validates a new SubAgentScope instance.
143
264
  * This factory method ensures that all tools provided in the prompt configuration
144
265
  * are valid for non-interactive use before creating the subagent instance.
266
+ *
267
+ * @plan PLAN-20251028-STATELESS6.P08
268
+ * @requirement REQ-STAT6-001.1, REQ-STAT6-003.1, REQ-STAT6-003.2
269
+ * @pseudocode agent-runtime-context.md lines 94-98 (steps 007.2-007.6)
270
+ *
145
271
  * @param {string} name - The name of the subagent.
146
- * @param {Config} runtimeContext - The shared runtime configuration and services.
272
+ * @param {Config} foregroundConfig - Foreground configuration used for shared scheduler plumbing.
147
273
  * @param {PromptConfig} promptConfig - Configuration for the subagent's prompt and behavior.
148
274
  * @param {ModelConfig} modelConfig - Configuration for the generative model parameters.
149
275
  * @param {RunConfig} runConfig - Configuration for the subagent's execution environment.
150
276
  * @param {ToolConfig} [toolConfig] - Optional configuration for tools.
151
277
  * @param {OutputConfig} [outputConfig] - Optional configuration for expected outputs.
278
+ * @param {SubAgentRuntimeOverrides} [overrides] - Optional stateless runtime inputs (provider runtime, adapters, settings) to bypass Config usage.
152
279
  * @returns {Promise<SubAgentScope>} A promise that resolves to a valid SubAgentScope instance.
153
280
  * @throws {Error} If any tool requires user confirmation.
154
281
  */
155
- static async create(name, runtimeContext, promptConfig, modelConfig, runConfig, toolConfig, outputConfig) {
282
+ static async create(name, foregroundConfig, promptConfig, modelConfig, runConfig, toolConfig, outputConfig, overrides = {}) {
283
+ const runtimeBundle = overrides.runtimeBundle;
284
+ if (!runtimeBundle) {
285
+ throw new Error('SubAgentScope.create requires a runtime bundle after initialization.');
286
+ }
287
+ const toolsView = runtimeBundle.runtimeContext.tools ?? runtimeBundle.toolsView;
288
+ if (!toolsView) {
289
+ throw new Error('SubAgentScope.create requires a ToolRegistryView from the runtime bundle.');
290
+ }
291
+ const toolRegistry = overrides.toolRegistry ?? runtimeBundle.toolRegistry;
292
+ if (!toolRegistry) {
293
+ throw new Error('SubAgentScope.create requires a ToolRegistry in the runtime bundle or overrides.');
294
+ }
156
295
  if (toolConfig) {
157
- const toolRegistry = runtimeContext.getToolRegistry();
158
- const toolsToLoad = [];
159
- for (const tool of toolConfig.tools) {
160
- if (typeof tool === 'string') {
161
- toolsToLoad.push(tool);
162
- }
296
+ await validateToolsAgainstRuntime({
297
+ toolConfig,
298
+ toolRegistry,
299
+ toolsView,
300
+ });
301
+ }
302
+ const settingsSnapshot = overrides.settingsSnapshot ?? runtimeBundle.settingsSnapshot;
303
+ const toolExecutorContext = createToolExecutionConfig(runtimeBundle, toolRegistry, settingsSnapshot, toolConfig);
304
+ const environmentContextLoader = overrides.environmentContextLoader ?? defaultEnvironmentContextLoader;
305
+ return new SubAgentScope(name, runtimeBundle.runtimeContext, modelConfig, runConfig, promptConfig, runtimeBundle.contentGenerator, toolExecutorContext, environmentContextLoader, foregroundConfig, toolConfig, outputConfig);
306
+ }
307
+ buildInitialMessages(context) {
308
+ const behaviourPrompts = context.get('task_behaviour_prompts') ?? [];
309
+ const initialInstruction = behaviourPrompts.length > 0
310
+ ? behaviourPrompts.join('\n')
311
+ : 'Follow the task directives provided in the system prompt.';
312
+ return [
313
+ {
314
+ role: 'user',
315
+ parts: [{ text: initialInstruction }],
316
+ },
317
+ ];
318
+ }
319
+ /**
320
+ * Executes the subagent in interactive mode by routing tool calls through the
321
+ * shared CoreToolScheduler. Tests may supply a custom schedulerFactory to
322
+ * observe scheduling behaviour without touching the real scheduler.
323
+ */
324
+ async runInteractive(context, options) {
325
+ const chat = await this.createChatObject(context);
326
+ if (!chat) {
327
+ this.output.terminate_reason = SubagentTerminateMode.ERROR;
328
+ return;
329
+ }
330
+ const abortController = new AbortController();
331
+ this.activeAbortController = abortController;
332
+ const functionDeclarations = this.buildRuntimeFunctionDeclarations();
333
+ if (this.outputConfig && this.outputConfig.outputs) {
334
+ functionDeclarations.push(...this.getScopeLocalFuncDefs());
335
+ }
336
+ if (functionDeclarations.length > 0 &&
337
+ typeof chat.setTools ===
338
+ 'function') {
339
+ try {
340
+ chat.setTools?.([
341
+ { functionDeclarations },
342
+ ]);
163
343
  }
164
- for (const toolName of toolsToLoad) {
165
- const tool = toolRegistry.getTool(toolName);
166
- if (tool) {
167
- const requiredParams = tool.schema.parameters?.required ?? [];
168
- if (requiredParams.length > 0) {
169
- // This check is imperfect. A tool might require parameters but still
170
- // be interactive (e.g., `delete_file(path)`). However, we cannot
171
- // build a generic invocation without knowing what dummy parameters
172
- // to provide. Crashing here because `build({})` fails is worse
173
- // than allowing a potential hang later if an interactive tool is
174
- // used. This is a best-effort check.
175
- console.warn(`Cannot check tool "${toolName}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`);
176
- continue;
344
+ catch (error) {
345
+ this.logger.warn(() => `Subagent ${this.subagentId} failed to register tools: ${error instanceof Error ? error.message : String(error)}`);
346
+ }
347
+ }
348
+ const schedulerConfig = this.createSchedulerConfig({ interactive: true });
349
+ let pendingCompletedCalls = null;
350
+ let completionResolver = null;
351
+ const awaitCompletedCalls = () => {
352
+ if (pendingCompletedCalls) {
353
+ const calls = pendingCompletedCalls;
354
+ pendingCompletedCalls = null;
355
+ return Promise.resolve(calls);
356
+ }
357
+ return new Promise((resolve) => {
358
+ completionResolver = resolve;
359
+ });
360
+ };
361
+ const outputUpdateHandler = (_toolCallId, output) => {
362
+ if (output && this.onMessage) {
363
+ this.onMessage(output);
364
+ }
365
+ };
366
+ const handleCompletion = async (calls) => {
367
+ if (completionResolver) {
368
+ completionResolver(calls);
369
+ completionResolver = null;
370
+ }
371
+ else {
372
+ pendingCompletedCalls = calls;
373
+ }
374
+ };
375
+ const scheduler = options?.schedulerFactory
376
+ ? options.schedulerFactory({
377
+ schedulerConfig,
378
+ onAllToolCallsComplete: handleCompletion,
379
+ outputUpdateHandler,
380
+ onToolCallsUpdate: undefined,
381
+ })
382
+ : new CoreToolScheduler({
383
+ config: schedulerConfig,
384
+ outputUpdateHandler,
385
+ onAllToolCallsComplete: handleCompletion,
386
+ onToolCallsUpdate: undefined,
387
+ getPreferredEditor: () => undefined,
388
+ onEditorClose: () => { },
389
+ });
390
+ const startTime = Date.now();
391
+ let turnCounter = 0;
392
+ let currentMessages = this.buildInitialMessages(context);
393
+ try {
394
+ while (true) {
395
+ if (this.runConfig.max_turns &&
396
+ turnCounter >= this.runConfig.max_turns) {
397
+ this.output.terminate_reason = SubagentTerminateMode.MAX_TURNS;
398
+ this.logger.warn(() => `Subagent ${this.subagentId} reached max turns (${this.runConfig.max_turns})`);
399
+ break;
400
+ }
401
+ let durationMin = (Date.now() - startTime) / (1000 * 60);
402
+ if (durationMin >= this.runConfig.max_time_minutes) {
403
+ this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
404
+ this.logger.warn(() => `Subagent ${this.subagentId} reached time limit (${this.runConfig.max_time_minutes} minutes)`);
405
+ break;
406
+ }
407
+ const currentTurn = turnCounter++;
408
+ const promptId = `${this.runtimeContext.state.sessionId}#${this.subagentId}#${currentTurn}`;
409
+ const providerName = this.runtimeContext.state.provider ?? 'backend';
410
+ const turn = new Turn(chat, promptId, this.subagentId, providerName);
411
+ let textResponse = '';
412
+ const parts = currentMessages[0]?.parts ?? [];
413
+ const stream = turn.run(parts, abortController.signal);
414
+ for await (const event of stream) {
415
+ if (abortController.signal.aborted) {
416
+ return;
417
+ }
418
+ if (event.type === GeminiEventType.Content && event.value) {
419
+ textResponse += event.value;
420
+ if (this.onMessage) {
421
+ this.onMessage(event.value);
422
+ }
177
423
  }
178
- const invocation = tool.build({});
179
- const confirmationDetails = await invocation.shouldConfirmExecute(new AbortController().signal);
180
- if (confirmationDetails) {
181
- throw new Error(`Tool "${toolName}" requires user confirmation and cannot be used in a non-interactive subagent.`);
424
+ else if (event.type === GeminiEventType.Error &&
425
+ event.value?.error) {
426
+ this.output.terminate_reason = SubagentTerminateMode.ERROR;
427
+ throw new Error(event.value.error.message);
182
428
  }
183
429
  }
430
+ if (textResponse.trim()) {
431
+ this.output.final_message = textResponse.trim();
432
+ }
433
+ const toolRequests = [...turn.pendingToolCalls];
434
+ if (toolRequests.length > 0) {
435
+ const manualParts = [];
436
+ const schedulerRequests = [];
437
+ for (const request of toolRequests) {
438
+ if (request.name === 'self.emitvalue') {
439
+ manualParts.push(...this.handleEmitValueCall(request));
440
+ }
441
+ else {
442
+ schedulerRequests.push(request);
443
+ }
444
+ }
445
+ let responseParts = [...manualParts];
446
+ if (schedulerRequests.length > 0) {
447
+ const completionPromise = awaitCompletedCalls();
448
+ await scheduler.schedule(schedulerRequests, abortController.signal);
449
+ const completedCalls = await completionPromise;
450
+ responseParts = responseParts.concat(this.buildPartsFromCompletedCalls(completedCalls));
451
+ const fatalCall = completedCalls.find((call) => call.status === 'error' &&
452
+ this.isFatalToolError(call.response.errorType));
453
+ if (fatalCall) {
454
+ const fatalMessage = this.buildToolUnavailableMessage(fatalCall.request.name, fatalCall.response.resultDisplay, fatalCall.response.error);
455
+ this.logger.warn(() => `Subagent ${this.subagentId} cannot use tool '${fatalCall.request.name}': ${fatalMessage}`);
456
+ responseParts.push({ text: fatalMessage });
457
+ this.output.final_message = fatalMessage;
458
+ }
459
+ }
460
+ if (responseParts.length === 0) {
461
+ responseParts.push({
462
+ text: 'All tool calls failed. Please analyze the errors and try an alternative approach.',
463
+ });
464
+ }
465
+ currentMessages = [{ role: 'user', parts: responseParts }];
466
+ continue;
467
+ }
468
+ durationMin = (Date.now() - startTime) / (1000 * 60);
469
+ if (durationMin >= this.runConfig.max_time_minutes) {
470
+ this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
471
+ this.logger.warn(() => `Subagent ${this.subagentId} reached time limit after turn ${currentTurn}`);
472
+ break;
473
+ }
474
+ const todoReminder = await this.buildTodoCompletionPrompt();
475
+ if (todoReminder) {
476
+ this.logger.debug(() => `Subagent ${this.subagentId} postponing completion until outstanding todos are addressed`);
477
+ currentMessages = [
478
+ {
479
+ role: 'user',
480
+ parts: [{ text: todoReminder }],
481
+ },
482
+ ];
483
+ continue;
484
+ }
485
+ if (!this.outputConfig ||
486
+ Object.keys(this.outputConfig.outputs).length === 0) {
487
+ this.output.terminate_reason = SubagentTerminateMode.GOAL;
488
+ break;
489
+ }
490
+ const remainingVars = Object.keys(this.outputConfig.outputs).filter((key) => !(key in this.output.emitted_vars));
491
+ if (remainingVars.length === 0) {
492
+ this.output.terminate_reason = SubagentTerminateMode.GOAL;
493
+ this.logger.debug(() => `Subagent ${this.subagentId} satisfied output requirements on turn ${currentTurn}`);
494
+ break;
495
+ }
496
+ const nudgeMessage = `You have stopped calling tools but have not emitted the following required variables: ${remainingVars.join(', ')}. Please use the 'self.emitvalue' tool to emit them now, or continue working if necessary.`;
497
+ this.logger.debug(() => `Subagent ${this.subagentId} nudging for outputs: ${remainingVars.join(', ')}`);
498
+ currentMessages = [
499
+ {
500
+ role: 'user',
501
+ parts: [{ text: nudgeMessage }],
502
+ },
503
+ ];
184
504
  }
505
+ this.finalizeOutput();
506
+ }
507
+ catch (error) {
508
+ this.logger.warn(() => `Error during subagent execution for ${this.subagentId}: ${error instanceof Error ? error.message : String(error)}`);
509
+ this.output.terminate_reason = SubagentTerminateMode.ERROR;
510
+ if (!this.output.final_message) {
511
+ this.output.final_message =
512
+ error instanceof Error ? error.message : String(error);
513
+ }
514
+ this.finalizeOutput();
515
+ throw error;
516
+ }
517
+ finally {
518
+ this.activeAbortController = null;
185
519
  }
186
- return new SubAgentScope(name, runtimeContext, promptConfig, modelConfig, runConfig, toolConfig, outputConfig);
187
520
  }
188
521
  /**
189
522
  * Runs the subagent in a non-interactive mode.
190
523
  * This method orchestrates the subagent's execution loop, including prompt templating,
191
524
  * tool execution, and termination conditions.
525
+ *
526
+ * @plan PLAN-20251028-STATELESS6.P08
527
+ * @requirement REQ-STAT6-001.1
528
+ *
192
529
  * @param {ContextState} context - The current context state containing variables for prompt templating.
193
530
  * @returns {Promise<void>} A promise that resolves when the subagent has completed its execution.
194
531
  */
@@ -199,28 +536,15 @@ export class SubAgentScope {
199
536
  return;
200
537
  }
201
538
  const abortController = new AbortController();
202
- const toolRegistry = this.runtimeContext.getToolRegistry();
203
- // Prepare the list of tools available to the subagent.
204
- const toolsList = [];
205
- if (this.toolConfig) {
206
- const toolsToLoad = [];
207
- for (const tool of this.toolConfig.tools) {
208
- if (typeof tool === 'string') {
209
- toolsToLoad.push(tool);
210
- }
211
- else {
212
- toolsList.push(tool);
213
- }
214
- }
215
- toolsList.push(...toolRegistry.getFunctionDeclarationsFiltered(toolsToLoad));
216
- }
217
- // Add local scope functions if outputs are expected.
539
+ this.activeAbortController = abortController;
540
+ const toolsList = this.buildRuntimeFunctionDeclarations();
218
541
  if (this.outputConfig && this.outputConfig.outputs) {
219
542
  toolsList.push(...this.getScopeLocalFuncDefs());
220
543
  }
221
- let currentMessages = [
222
- { role: 'user', parts: [{ text: 'Get Started!' }] },
223
- ];
544
+ this.logger.debug(() => `Subagent ${this.subagentId} (${this.name}) starting run with toolCount=${toolsList.length} requestedOutputs=${this.outputConfig
545
+ ? Object.keys(this.outputConfig.outputs).join(', ')
546
+ : 'none'} runConfig=${JSON.stringify(this.runConfig)}`);
547
+ let currentMessages = this.buildInitialMessages(context);
224
548
  const startTime = Date.now();
225
549
  let turnCounter = 0;
226
550
  try {
@@ -229,14 +553,20 @@ export class SubAgentScope {
229
553
  if (this.runConfig.max_turns &&
230
554
  turnCounter >= this.runConfig.max_turns) {
231
555
  this.output.terminate_reason = SubagentTerminateMode.MAX_TURNS;
556
+ this.logger.warn(() => `Subagent ${this.subagentId} reached max turns (${this.runConfig.max_turns})`);
232
557
  break;
233
558
  }
234
559
  let durationMin = (Date.now() - startTime) / (1000 * 60);
235
560
  if (durationMin >= this.runConfig.max_time_minutes) {
236
561
  this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
562
+ this.logger.warn(() => `Subagent ${this.subagentId} reached time limit (${this.runConfig.max_time_minutes} minutes)`);
237
563
  break;
238
564
  }
239
- const promptId = `${this.runtimeContext.getSessionId()}#${this.subagentId}#${turnCounter++}`;
565
+ // @plan PLAN-20251028-STATELESS6.P08
566
+ // @requirement REQ-STAT6-001.1
567
+ const currentTurn = turnCounter++;
568
+ const promptId = `${this.runtimeContext.state.sessionId}#${this.subagentId}#${currentTurn}`;
569
+ this.logger.debug(() => `Subagent ${this.subagentId} turn=${currentTurn} promptId=${promptId}`);
240
570
  const messageParams = {
241
571
  message: currentMessages[0]?.parts || [],
242
572
  config: {
@@ -245,24 +575,69 @@ export class SubAgentScope {
245
575
  },
246
576
  };
247
577
  const responseStream = await chat.sendMessageStream(messageParams, promptId);
248
- const functionCalls = [];
578
+ let functionCalls = [];
249
579
  let textResponse = '';
250
580
  for await (const resp of responseStream) {
251
581
  if (abortController.signal.aborted)
252
582
  return;
253
583
  if (resp.type === StreamEventType.CHUNK && resp.value.functionCalls) {
254
- functionCalls.push(...resp.value.functionCalls);
584
+ const chunkCalls = resp.value.functionCalls ?? [];
585
+ if (chunkCalls.length > 0) {
586
+ functionCalls.push(...chunkCalls);
587
+ this.logger.debug(() => `Subagent ${this.subagentId} received ${chunkCalls.length} function calls on turn ${currentTurn}`);
588
+ }
255
589
  }
256
590
  if (resp.type === StreamEventType.CHUNK && resp.value.text) {
257
591
  textResponse += resp.value.text;
258
592
  }
259
593
  }
260
- if (this.onMessage && textResponse) {
261
- this.onMessage(textResponse);
594
+ if (textResponse) {
595
+ if (this.onMessage) {
596
+ this.onMessage(textResponse);
597
+ }
598
+ let cleanedText = textResponse;
599
+ try {
600
+ const parsedResult = this.textToolParser.parse(textResponse);
601
+ cleanedText = parsedResult.cleanedContent;
602
+ if (parsedResult.toolCalls.length > 0) {
603
+ const synthesizedCalls = [];
604
+ parsedResult.toolCalls.forEach((call, index) => {
605
+ const normalizedName = this.normalizeToolName(call.name);
606
+ if (!normalizedName) {
607
+ this.logger.debug(() => `Subagent ${this.subagentId} could not map textual tool name '${call.name}' to a registered tool`);
608
+ return;
609
+ }
610
+ synthesizedCalls.push({
611
+ id: `parsed_${this.subagentId}_${Date.now()}_${index}`,
612
+ name: normalizedName,
613
+ args: call.arguments ?? {},
614
+ });
615
+ });
616
+ if (synthesizedCalls.length > 0) {
617
+ functionCalls = [...functionCalls, ...synthesizedCalls];
618
+ this.logger.debug(() => `Subagent ${this.subagentId} extracted ${synthesizedCalls.length} tool call(s) from text: ${synthesizedCalls
619
+ .map((call) => call.name)
620
+ .join(', ')}`);
621
+ }
622
+ }
623
+ }
624
+ catch (error) {
625
+ this.logger.warn(() => `Subagent ${this.subagentId} failed to parse textual tool calls: ${error instanceof Error ? error.message : String(error)}`);
626
+ }
627
+ textResponse = cleanedText;
628
+ const trimmedText = textResponse.trim();
629
+ if (trimmedText.length > 0) {
630
+ this.output.final_message = trimmedText;
631
+ }
632
+ const preview = textResponse.length > 200
633
+ ? `${textResponse.slice(0, 200)}…`
634
+ : textResponse;
635
+ this.logger.debug(() => `Subagent ${this.subagentId} model response (truncated): ${preview}`);
262
636
  }
263
637
  durationMin = (Date.now() - startTime) / (1000 * 60);
264
638
  if (durationMin >= this.runConfig.max_time_minutes) {
265
639
  this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
640
+ this.logger.warn(() => `Subagent ${this.subagentId} reached time limit after turn ${currentTurn}`);
266
641
  break;
267
642
  }
268
643
  if (functionCalls.length > 0) {
@@ -270,6 +645,17 @@ export class SubAgentScope {
270
645
  }
271
646
  else {
272
647
  // Model stopped calling tools. Check if goal is met.
648
+ const todoReminder = await this.buildTodoCompletionPrompt();
649
+ if (todoReminder) {
650
+ this.logger.debug(() => `Subagent ${this.subagentId} postponing completion until outstanding todos are addressed`);
651
+ currentMessages = [
652
+ {
653
+ role: 'user',
654
+ parts: [{ text: todoReminder }],
655
+ },
656
+ ];
657
+ continue;
658
+ }
273
659
  if (!this.outputConfig ||
274
660
  Object.keys(this.outputConfig.outputs).length === 0) {
275
661
  this.output.terminate_reason = SubagentTerminateMode.GOAL;
@@ -278,10 +664,11 @@ export class SubAgentScope {
278
664
  const remainingVars = Object.keys(this.outputConfig.outputs).filter((key) => !(key in this.output.emitted_vars));
279
665
  if (remainingVars.length === 0) {
280
666
  this.output.terminate_reason = SubagentTerminateMode.GOAL;
667
+ this.logger.debug(() => `Subagent ${this.subagentId} satisfied output requirements on turn ${currentTurn}`);
281
668
  break;
282
669
  }
283
670
  const nudgeMessage = `You have stopped calling tools but have not emitted the following required variables: ${remainingVars.join(', ')}. Please use the 'self.emitvalue' tool to emit them now, or continue working if necessary.`;
284
- console.debug(nudgeMessage);
671
+ this.logger.debug(() => `Subagent ${this.subagentId} nudging for outputs: ${remainingVars.join(', ')}`);
285
672
  currentMessages = [
286
673
  {
287
674
  role: 'user',
@@ -290,12 +677,34 @@ export class SubAgentScope {
290
677
  ];
291
678
  }
292
679
  }
680
+ this.finalizeOutput();
293
681
  }
294
682
  catch (error) {
295
- console.error('Error during subagent execution:', error);
683
+ this.logger.warn(() => `Error during subagent execution for ${this.subagentId}: ${error instanceof Error ? error.message : String(error)}`);
296
684
  this.output.terminate_reason = SubagentTerminateMode.ERROR;
685
+ if (!this.output.final_message) {
686
+ this.output.final_message =
687
+ error instanceof Error ? error.message : String(error);
688
+ }
689
+ this.finalizeOutput();
297
690
  throw error;
298
691
  }
692
+ finally {
693
+ this.activeAbortController = null;
694
+ }
695
+ }
696
+ cancel(reason) {
697
+ if (this.activeAbortController?.signal.aborted) {
698
+ return;
699
+ }
700
+ if (!this.activeAbortController) {
701
+ return;
702
+ }
703
+ this.logger.warn(() => {
704
+ const suffix = reason ? `: ${reason}` : '';
705
+ return `Subagent ${this.subagentId} cancellation requested${suffix}`;
706
+ });
707
+ this.activeAbortController.abort();
299
708
  }
300
709
  /**
301
710
  * Processes a list of function calls, executing each one and collecting their responses.
@@ -318,7 +727,9 @@ export class SubAgentScope {
318
727
  args: (functionCall.args ?? {}),
319
728
  isClientInitiated: true,
320
729
  prompt_id: promptId,
730
+ agentId: this.subagentId,
321
731
  };
732
+ this.logger.debug(() => `Subagent ${this.subagentId} executing tool '${requestInfo.name}' with args=${JSON.stringify(requestInfo.args)}`);
322
733
  let toolResponse;
323
734
  // Handle scope-local tools first.
324
735
  if (functionCall.name === 'self.emitvalue') {
@@ -330,14 +741,31 @@ export class SubAgentScope {
330
741
  responseParts: [{ text: `Emitted variable ${valName} successfully` }],
331
742
  resultDisplay: `Emitted variable ${valName} successfully`,
332
743
  error: undefined,
744
+ errorType: undefined,
745
+ agentId: requestInfo.agentId,
333
746
  };
334
747
  }
335
748
  else {
336
- toolResponse = await executeToolCall(this.runtimeContext, requestInfo, abortController.signal);
749
+ // @plan PLAN-20251028-STATELESS6.P08
750
+ // @requirement REQ-STAT6-001.1
751
+ toolResponse = await executeToolCall(this.toolExecutorContext, requestInfo, abortController.signal);
337
752
  }
338
753
  if (toolResponse.error) {
339
754
  console.error(`Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`);
340
755
  }
756
+ if (toolResponse.error) {
757
+ this.logger.warn(() => `Subagent ${this.subagentId} tool '${functionCall.name}' failed: ${toolResponse.error?.message}`);
758
+ }
759
+ else {
760
+ this.logger.debug(() => `Subagent ${this.subagentId} tool '${functionCall.name}' completed successfully`);
761
+ }
762
+ if (this.isFatalToolError(toolResponse.errorType)) {
763
+ const fatalMessage = this.buildToolUnavailableMessage(functionCall.name, toolResponse.resultDisplay, toolResponse.error);
764
+ this.logger.warn(() => `Subagent ${this.subagentId} cannot use tool '${functionCall.name}': ${fatalMessage}`);
765
+ toolResponseParts.push({ text: fatalMessage });
766
+ this.output.final_message = fatalMessage;
767
+ continue;
768
+ }
341
769
  if (toolResponse.responseParts) {
342
770
  toolResponseParts.push(...toolResponse.responseParts);
343
771
  }
@@ -350,6 +778,236 @@ export class SubAgentScope {
350
778
  }
351
779
  return [{ role: 'user', parts: toolResponseParts }];
352
780
  }
781
+ createSchedulerConfig(options) {
782
+ const isInteractive = options?.interactive ?? false;
783
+ const whitelist = !isInteractive && this.toolConfig
784
+ ? this.toolConfig.tools.filter((entry) => typeof entry === 'string')
785
+ : [];
786
+ const getEphemeralSettings = typeof this.toolExecutorContext.getEphemeralSettings === 'function'
787
+ ? () => ({
788
+ ...this.toolExecutorContext.getEphemeralSettings(),
789
+ })
790
+ : () => this.config.getEphemeralSettings();
791
+ const getExcludeTools = typeof this.toolExecutorContext.getExcludeTools === 'function'
792
+ ? () => this.toolExecutorContext.getExcludeTools()
793
+ : () => this.config.getExcludeTools?.() ?? [];
794
+ const getTelemetryLogPromptsEnabled = typeof this.toolExecutorContext.getTelemetryLogPromptsEnabled ===
795
+ 'function'
796
+ ? () => this.toolExecutorContext.getTelemetryLogPromptsEnabled()
797
+ : () => this.config.getTelemetryLogPromptsEnabled();
798
+ const allowedTools = isInteractive
799
+ ? typeof this.config.getAllowedTools === 'function'
800
+ ? this.config.getAllowedTools()
801
+ : undefined
802
+ : whitelist.length > 0
803
+ ? whitelist
804
+ : typeof this.config.getAllowedTools === 'function'
805
+ ? this.config.getAllowedTools()
806
+ : undefined;
807
+ return {
808
+ getToolRegistry: () => this.toolExecutorContext.getToolRegistry(),
809
+ getSessionId: () => this.toolExecutorContext.getSessionId(),
810
+ getEphemeralSettings,
811
+ getExcludeTools,
812
+ getTelemetryLogPromptsEnabled,
813
+ getAllowedTools: () => allowedTools,
814
+ getApprovalMode: () => typeof this.config.getApprovalMode === 'function'
815
+ ? this.config.getApprovalMode()
816
+ : ApprovalMode.DEFAULT,
817
+ };
818
+ }
819
+ finalizeOutput() {
820
+ const message = this.output.final_message;
821
+ if (typeof message === 'string' && message.trim().length > 0) {
822
+ return;
823
+ }
824
+ const emittedVars = this.output.emitted_vars ?? {};
825
+ const emittedEntries = Object.entries(emittedVars)
826
+ .filter(([, value]) => value !== undefined &&
827
+ value !== null &&
828
+ String(value).trim().length > 0)
829
+ .map(([key, value]) => `${key}=${String(value)}`);
830
+ let baseMessage;
831
+ switch (this.output.terminate_reason) {
832
+ case SubagentTerminateMode.GOAL:
833
+ baseMessage = 'Completed the requested task.';
834
+ break;
835
+ case SubagentTerminateMode.TIMEOUT:
836
+ baseMessage = 'Stopped because the time limit was reached.';
837
+ break;
838
+ case SubagentTerminateMode.MAX_TURNS:
839
+ baseMessage =
840
+ 'Stopped because the maximum number of turns was reached.';
841
+ break;
842
+ case SubagentTerminateMode.ERROR:
843
+ default:
844
+ baseMessage = 'Stopped due to an unrecoverable error.';
845
+ break;
846
+ }
847
+ const varsSuffix = emittedEntries.length > 0
848
+ ? ` Emitted variables: ${emittedEntries.join(', ')}.`
849
+ : '';
850
+ this.output.final_message = `${baseMessage}${varsSuffix}`.trim();
851
+ }
852
+ isFatalToolError(errorType) {
853
+ return (errorType === ToolErrorType.TOOL_DISABLED ||
854
+ errorType === ToolErrorType.TOOL_NOT_REGISTERED);
855
+ }
856
+ buildToolUnavailableMessage(toolName, resultDisplay, error) {
857
+ const detail = this.extractToolDetail(resultDisplay, error);
858
+ const baseMessage = `Tool "${toolName}" is not available in this environment.`;
859
+ return detail
860
+ ? `${baseMessage} ${detail}`
861
+ : `${baseMessage} Please continue without using it.`;
862
+ }
863
+ extractToolDetail(resultDisplay, error) {
864
+ if (error?.message) {
865
+ return error.message;
866
+ }
867
+ if (typeof resultDisplay === 'string') {
868
+ return resultDisplay;
869
+ }
870
+ if (resultDisplay &&
871
+ typeof resultDisplay === 'object' &&
872
+ 'message' in resultDisplay &&
873
+ typeof resultDisplay.message === 'string') {
874
+ return resultDisplay.message;
875
+ }
876
+ return undefined;
877
+ }
878
+ handleEmitValueCall(request) {
879
+ const args = request.args ?? {};
880
+ const variableName = typeof args.emit_variable_name === 'string'
881
+ ? args.emit_variable_name
882
+ : typeof args.emitVariableName === 'string'
883
+ ? args.emitVariableName
884
+ : '';
885
+ const variableValue = typeof args.emit_variable_value === 'string'
886
+ ? args.emit_variable_value
887
+ : typeof args.emitVariableValue === 'string'
888
+ ? args.emitVariableValue
889
+ : '';
890
+ if (variableName && variableValue) {
891
+ this.output.emitted_vars[variableName] = variableValue;
892
+ const message = `Emitted variable ${variableName} successfully`;
893
+ if (this.onMessage) {
894
+ this.onMessage(`[${this.subagentId}] ${message}`);
895
+ }
896
+ return [
897
+ {
898
+ functionCall: {
899
+ id: request.callId,
900
+ name: request.name,
901
+ args: request.args,
902
+ },
903
+ },
904
+ {
905
+ functionResponse: {
906
+ id: request.callId,
907
+ name: request.name,
908
+ response: {
909
+ emit_variable_name: variableName,
910
+ emit_variable_value: variableValue,
911
+ message,
912
+ },
913
+ },
914
+ },
915
+ ];
916
+ }
917
+ const errorMessage = 'self.emitvalue requires emit_variable_name and emit_variable_value arguments.';
918
+ this.logger.warn(() => `Subagent ${this.subagentId} failed to emit value: ${errorMessage}`);
919
+ return [
920
+ {
921
+ functionCall: {
922
+ id: request.callId,
923
+ name: request.name,
924
+ args: request.args,
925
+ },
926
+ },
927
+ {
928
+ functionResponse: {
929
+ id: request.callId,
930
+ name: request.name,
931
+ response: { error: errorMessage },
932
+ },
933
+ },
934
+ ];
935
+ }
936
+ buildPartsFromCompletedCalls(completedCalls) {
937
+ const aggregate = [];
938
+ for (const call of completedCalls) {
939
+ if (call.response?.responseParts?.length) {
940
+ aggregate.push(...call.response.responseParts);
941
+ }
942
+ else {
943
+ aggregate.push({
944
+ text: `Tool ${call.request.name} completed without response.`,
945
+ });
946
+ }
947
+ if (call.status === 'error') {
948
+ const errorMessage = call.response?.error?.message ??
949
+ call.response?.resultDisplay ??
950
+ 'Tool execution failed.';
951
+ this.logger.warn(() => `Subagent ${this.subagentId} tool '${call.request.name}' failed: ${errorMessage}`);
952
+ }
953
+ else if (call.status === 'cancelled') {
954
+ this.logger.warn(() => `Subagent ${this.subagentId} tool '${call.request.name}' was cancelled.`);
955
+ }
956
+ const display = call.response?.resultDisplay;
957
+ if (typeof display === 'string' && this.onMessage && display.trim()) {
958
+ this.onMessage(display);
959
+ }
960
+ }
961
+ return aggregate;
962
+ }
963
+ async buildTodoCompletionPrompt() {
964
+ const sessionId = this.runtimeContext.state.sessionId;
965
+ if (!sessionId) {
966
+ return null;
967
+ }
968
+ try {
969
+ let todos = await new TodoStore(sessionId, this.subagentId).readTodos();
970
+ if (todos.length === 0) {
971
+ todos = await new TodoStore(sessionId).readTodos();
972
+ }
973
+ if (todos.length === 0) {
974
+ return null;
975
+ }
976
+ const outstanding = todos.filter((todo) => todo.status !== 'completed');
977
+ if (outstanding.length === 0) {
978
+ return null;
979
+ }
980
+ const previewCount = Math.min(3, outstanding.length);
981
+ const previewLines = outstanding
982
+ .slice(0, previewCount)
983
+ .map((todo) => `- ${todo.content}`);
984
+ if (outstanding.length > previewCount) {
985
+ previewLines.push(`- ... and ${outstanding.length - previewCount} more`);
986
+ }
987
+ return [
988
+ 'You still have todos in your todo list. Complete them before finishing.',
989
+ previewLines.length > 0
990
+ ? `Outstanding items:\n${previewLines.join('\n')}`
991
+ : undefined,
992
+ ]
993
+ .filter(Boolean)
994
+ .join('\n\n');
995
+ }
996
+ catch (error) {
997
+ this.logger.warn(() => `Subagent ${this.subagentId} could not inspect todos: ${error instanceof Error ? error.message : String(error)}`);
998
+ return null;
999
+ }
1000
+ }
1001
+ /**
1002
+ * Creates a GeminiChat instance for this subagent.
1003
+ *
1004
+ * @plan PLAN-20251028-STATELESS6.P08
1005
+ * @requirement REQ-STAT6-001.1, REQ-STAT6-003.1
1006
+ * @pseudocode agent-runtime-context.md line 99 (step 007.7)
1007
+ *
1008
+ * Step 007.7: Update GeminiChat instantiation to use AgentRuntimeContext
1009
+ * Step 007.8: REMOVE Config mutation (no setModel call)
1010
+ */
353
1011
  async createChatObject(context) {
354
1012
  if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
355
1013
  throw new Error('PromptConfig must have either `systemPrompt` or `initialMessages` defined.');
@@ -357,37 +1015,45 @@ export class SubAgentScope {
357
1015
  if (this.promptConfig.systemPrompt && this.promptConfig.initialMessages) {
358
1016
  throw new Error('PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.');
359
1017
  }
360
- const envParts = await getEnvironmentContext(this.runtimeContext);
1018
+ const envParts = await this.environmentContextLoader(this.runtimeContext);
361
1019
  // Extract environment context text
362
1020
  const envContextText = envParts
363
1021
  .map((part) => ('text' in part ? part.text : ''))
364
1022
  .join('\n');
365
1023
  const start_history = [...(this.promptConfig.initialMessages ?? [])];
366
1024
  // Build system instruction with environment context
367
- let systemInstruction = this.promptConfig.systemPrompt
1025
+ const personaPrompt = this.promptConfig.systemPrompt
368
1026
  ? this.buildChatSystemPrompt(context)
369
- : envContextText;
370
- // If we have both env context and system prompt, combine them
371
- if (this.promptConfig.systemPrompt && envContextText) {
372
- systemInstruction = `${envContextText}\n\n${systemInstruction}`;
1027
+ : '';
1028
+ let systemInstruction = personaPrompt;
1029
+ if (envContextText && envContextText.trim().length > 0) {
1030
+ systemInstruction = personaPrompt
1031
+ ? `${personaPrompt}\n\n${envContextText.trim()}`
1032
+ : envContextText.trim();
373
1033
  }
1034
+ this.logger.debug(() => {
1035
+ const preview = systemInstruction && systemInstruction.length > 0
1036
+ ? systemInstruction.slice(0, 1200)
1037
+ : '<empty>';
1038
+ return `System instruction preview: ${preview}`;
1039
+ });
374
1040
  try {
1041
+ // Step 007.7: Build generation config from runtime view ephemerals
1042
+ // @plan PLAN-20251028-STATELESS6.P08
1043
+ // @requirement REQ-STAT6-002.2
375
1044
  const generationConfig = {
376
1045
  temperature: this.modelConfig.temp,
377
1046
  topP: this.modelConfig.top_p,
1047
+ systemInstruction: systemInstruction || undefined,
378
1048
  };
379
- if (systemInstruction) {
380
- generationConfig.systemInstruction = systemInstruction;
381
- }
382
- const contentGenConfig = this.runtimeContext.getContentGeneratorConfig();
383
- if (!contentGenConfig) {
384
- // In llxprt, when using providers, we may not have a content generator config
385
- // but we still need to create one for the subagent
386
- throw new Error('Content generator config is not available. Please ensure proper initialization.');
387
- }
388
- const contentGenerator = await createContentGenerator(contentGenConfig, this.runtimeContext, this.runtimeContext.getSessionId());
389
- this.runtimeContext.setModel(this.modelConfig.model);
390
- return new GeminiChat(this.runtimeContext, contentGenerator, generationConfig, start_history);
1049
+ // Step 007.7: Instantiate GeminiChat with runtime view
1050
+ // @plan PLAN-20251028-STATELESS6.P10
1051
+ // @requirement REQ-STAT6-001.2, REQ-STAT6-003.1, REQ-STAT6-003.2
1052
+ // @pseudocode agent-runtime-context.md line 99 (step 007.7)
1053
+ // NOTE: NO Config.setModel() call - REQ-STAT6-003.1 (step 007.8)
1054
+ // NOTE: GeminiChat operates solely on runtime context
1055
+ return new GeminiChat(this.runtimeContext, // AgentRuntimeContext (replaces runtimeState+config+history)
1056
+ this.contentGenerator, generationConfig, start_history);
391
1057
  }
392
1058
  catch (error) {
393
1059
  await reportError(error, 'Error initializing Gemini chat session.', start_history, 'startChat');
@@ -395,6 +1061,35 @@ export class SubAgentScope {
395
1061
  return undefined;
396
1062
  }
397
1063
  }
1064
+ buildRuntimeFunctionDeclarations() {
1065
+ if (!this.toolConfig || this.toolConfig.tools.length === 0) {
1066
+ return [];
1067
+ }
1068
+ const toolsView = this.runtimeContext.tools;
1069
+ const listedNames = typeof toolsView.listToolNames === 'function'
1070
+ ? toolsView.listToolNames()
1071
+ : [];
1072
+ const allowedNames = new Set(listedNames.map(normalizeToolName));
1073
+ const declarations = [];
1074
+ for (const entry of this.toolConfig.tools) {
1075
+ if (typeof entry !== 'string') {
1076
+ declarations.push(entry);
1077
+ continue;
1078
+ }
1079
+ if (allowedNames.size > 0 &&
1080
+ !allowedNames.has(normalizeToolName(entry))) {
1081
+ console.warn(`Tool "${entry}" is not permitted by the runtime view and will be skipped.`);
1082
+ continue;
1083
+ }
1084
+ const metadata = toolsView.getToolMetadata(entry);
1085
+ if (!metadata) {
1086
+ console.warn(`Tool "${entry}" is not available in the runtime view and will be skipped.`);
1087
+ continue;
1088
+ }
1089
+ declarations.push(convertMetadataToFunctionDeclaration(entry, metadata));
1090
+ }
1091
+ return declarations;
1092
+ }
398
1093
  /**
399
1094
  * Returns an array of FunctionDeclaration objects for tools that are local to the subagent's scope.
400
1095
  * Currently, this includes the `self.emitvalue` tool for emitting variables.
@@ -422,6 +1117,41 @@ export class SubAgentScope {
422
1117
  };
423
1118
  return [emitValueTool];
424
1119
  }
1120
+ normalizeToolName(rawName) {
1121
+ if (!rawName) {
1122
+ return null;
1123
+ }
1124
+ const candidates = new Set();
1125
+ const trimmed = rawName.trim();
1126
+ if (trimmed) {
1127
+ candidates.add(trimmed);
1128
+ candidates.add(trimmed.toLowerCase());
1129
+ }
1130
+ if (trimmed.endsWith('Tool')) {
1131
+ const withoutSuffix = trimmed.slice(0, -4);
1132
+ if (withoutSuffix) {
1133
+ candidates.add(withoutSuffix);
1134
+ candidates.add(withoutSuffix.toLowerCase());
1135
+ candidates.add(this.toSnakeCase(withoutSuffix));
1136
+ }
1137
+ }
1138
+ candidates.add(this.toSnakeCase(trimmed));
1139
+ for (const candidate of candidates) {
1140
+ if (!candidate) {
1141
+ continue;
1142
+ }
1143
+ if (this.runtimeContext.tools.getToolMetadata(candidate)) {
1144
+ return candidate;
1145
+ }
1146
+ }
1147
+ return null;
1148
+ }
1149
+ toSnakeCase(value) {
1150
+ return value
1151
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
1152
+ .replace(/[\s-]+/g, '_')
1153
+ .toLowerCase();
1154
+ }
425
1155
  /**
426
1156
  * Builds the system prompt for the chat based on the provided configurations.
427
1157
  * It templates the base system prompt and appends instructions for emitting