@poncho-ai/harness 0.50.4 → 0.50.5
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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +32 -1
- package/dist/index.js +55 -17
- package/package.json +1 -1
- package/src/harness.ts +13 -1
- package/src/orchestrator/index.ts +2 -0
- package/src/orchestrator/orchestrator.ts +94 -17
- package/test/orchestrator.test.ts +49 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.50.
|
|
2
|
+
> @poncho-ai/harness@0.50.5 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m535.57 KB[39m
|
|
12
12
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 240ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 7598ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m91.35 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.50.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`991a4b9`](https://github.com/cesr/poncho-ai/commit/991a4b98d6683c105c7aae50551d30b16080d618) Thanks [@cesr](https://github.com/cesr)! - harness: subagents survive the wall-clock timeout, and can be given a longer budget than the foreground turn.
|
|
8
|
+
|
|
9
|
+
Previously a subagent that hit its hard `timeout` (vs. `maxSteps`) emitted `run:error` with no `runResult`, so the orchestrator dropped everything it had gathered: the parent received a bare `(no result)`, the subagent was falsely marked `completed`, and the work — often dozens of completed searches, just short of the write step — was lost.
|
|
10
|
+
- **Graceful timeout/error delivery.** When a subagent run ends abnormally (timeout or model error) with no `runResult`, the orchestrator now recovers its real output (run response → streamed draft → transcript walk-back, discarding the synthetic `[Error: …]` placeholder), and delivers it tagged so the parent knows it didn't finish — it may not have written its files — with a concrete recovery hint (use the partial work, send a write-only `message_subagent` follow-up, or `read_subagent(…, mode:"full")`). The subagent is marked `status: "error"` (not a fake `completed`) and carries the failure in its `error` field. Applied to both the spawn and continuation paths.
|
|
11
|
+
- **`runTimeoutSecOverride` (HarnessOptions).** A constructor-level override for the per-run hard wall-clock timeout, taking precedence over the agent definition's `limits.timeout`. Lets a platform give background subagents a longer budget (e.g. 1h) than a foreground turn (5 min) without forking the agent definition. `0` disables the hard timeout.
|
|
12
|
+
|
|
3
13
|
## 0.50.4
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1261,6 +1261,14 @@ interface HarnessOptions {
|
|
|
1261
1261
|
* should also be browsable in the VFS. Empty by default.
|
|
1262
1262
|
*/
|
|
1263
1263
|
systemSkillPaths?: string[];
|
|
1264
|
+
/**
|
|
1265
|
+
* Override the per-run hard wall-clock timeout, in seconds, taking
|
|
1266
|
+
* precedence over the agent definition's `limits.timeout`. Platforms use
|
|
1267
|
+
* this to give background subagents a longer budget than the foreground
|
|
1268
|
+
* agent without forking the agent definition (e.g. a 1h research subagent
|
|
1269
|
+
* vs. a 5-min foreground turn). `0` disables the hard timeout.
|
|
1270
|
+
*/
|
|
1271
|
+
runTimeoutSecOverride?: number;
|
|
1264
1272
|
}
|
|
1265
1273
|
interface HarnessRunOutput {
|
|
1266
1274
|
runId: string;
|
|
@@ -1280,6 +1288,7 @@ interface ArchivedToolResult {
|
|
|
1280
1288
|
declare class AgentHarness {
|
|
1281
1289
|
private readonly workingDir;
|
|
1282
1290
|
private readonly environment;
|
|
1291
|
+
private readonly runTimeoutSecOverride?;
|
|
1283
1292
|
private modelProvider;
|
|
1284
1293
|
private readonly modelProviderInjected;
|
|
1285
1294
|
private readonly dispatcher;
|
|
@@ -1988,6 +1997,28 @@ declare const STALE_SUBAGENT_THRESHOLD_MS: number;
|
|
|
1988
1997
|
* before — instead of surfacing to the parent as an empty result.
|
|
1989
1998
|
*/
|
|
1990
1999
|
declare const lastAssistantText: (messages: Message[]) => string;
|
|
2000
|
+
/**
|
|
2001
|
+
* The run loop stuffs a synthetic `[Error: ...]` placeholder into the draft /
|
|
2002
|
+
* persisted assistant text when a run ends on `run:error` (e.g. a timeout).
|
|
2003
|
+
* That placeholder is not real model output — strip it so we don't surface it
|
|
2004
|
+
* to the parent as the subagent's "response".
|
|
2005
|
+
*/
|
|
2006
|
+
declare const realResponseText: (text: string | undefined) => string;
|
|
2007
|
+
/**
|
|
2008
|
+
* Build the result text delivered to the parent when a subagent ended
|
|
2009
|
+
* abnormally (timeout / error) with no RunResult. We never drop the work it
|
|
2010
|
+
* gathered, and the parent is told it didn't finish — e.g. it may not have
|
|
2011
|
+
* written its output files — plus how to recover (use what's here, send a
|
|
2012
|
+
* write-only follow-up, or read the full transcript).
|
|
2013
|
+
*/
|
|
2014
|
+
declare const abnormalEndResponse: (opts: {
|
|
2015
|
+
subagentId: string;
|
|
2016
|
+
gathered: string;
|
|
2017
|
+
runError?: {
|
|
2018
|
+
code?: string;
|
|
2019
|
+
message?: string;
|
|
2020
|
+
};
|
|
2021
|
+
}) => string;
|
|
1991
2022
|
type ActiveConversationRun = {
|
|
1992
2023
|
ownerId: string;
|
|
1993
2024
|
abortController: AbortController;
|
|
@@ -2150,4 +2181,4 @@ interface RunConversationTurnResult {
|
|
|
2150
2181
|
}
|
|
2151
2182
|
declare const runConversationTurn: (opts: RunConversationTurnOpts) => Promise<RunConversationTurnResult>;
|
|
2152
2183
|
|
|
2153
|
-
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
|
2184
|
+
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
package/dist/index.js
CHANGED
|
@@ -9170,6 +9170,7 @@ function extractMediaFromToolOutput(output) {
|
|
|
9170
9170
|
var AgentHarness = class _AgentHarness {
|
|
9171
9171
|
workingDir;
|
|
9172
9172
|
environment;
|
|
9173
|
+
runTimeoutSecOverride;
|
|
9173
9174
|
modelProvider;
|
|
9174
9175
|
modelProviderInjected;
|
|
9175
9176
|
dispatcher = new ToolDispatcher();
|
|
@@ -9375,6 +9376,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
9375
9376
|
constructor(options = {}) {
|
|
9376
9377
|
this.workingDir = options.workingDir ?? process.cwd();
|
|
9377
9378
|
this.environment = options.environment ?? "development";
|
|
9379
|
+
this.runTimeoutSecOverride = options.runTimeoutSecOverride;
|
|
9378
9380
|
this.modelProviderInjected = !!options.modelProvider;
|
|
9379
9381
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
9380
9382
|
this.uploadStore = options.uploadStore;
|
|
@@ -10237,7 +10239,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
10237
10239
|
const runId = `run_${randomUUID5()}`;
|
|
10238
10240
|
const start = now();
|
|
10239
10241
|
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 20;
|
|
10240
|
-
const configuredTimeout = agent.frontmatter.limits?.timeout;
|
|
10242
|
+
const configuredTimeout = this.runTimeoutSecOverride ?? agent.frontmatter.limits?.timeout;
|
|
10241
10243
|
const timeoutMs = this.environment === "development" && configuredTimeout == null ? 0 : (configuredTimeout ?? 300) * 1e3;
|
|
10242
10244
|
const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
|
|
10243
10245
|
const softDeadlineMs = input.disableSoftDeadline || platformMaxDurationSec <= 0 ? 0 : platformMaxDurationSec * 800;
|
|
@@ -12340,6 +12342,18 @@ var lastAssistantText = (messages) => {
|
|
|
12340
12342
|
}
|
|
12341
12343
|
return "";
|
|
12342
12344
|
};
|
|
12345
|
+
var realResponseText = (text) => {
|
|
12346
|
+
const t = (text ?? "").trim();
|
|
12347
|
+
return t.startsWith("[Error:") ? "" : t;
|
|
12348
|
+
};
|
|
12349
|
+
var abnormalEndResponse = (opts) => {
|
|
12350
|
+
const timedOut = opts.runError?.code === "TIMEOUT";
|
|
12351
|
+
const head = timedOut ? "[Subagent hit its time limit before finishing \u2014 it may not have written its output files.]" : `[Subagent ended before finishing${opts.runError?.message ? `: ${opts.runError.message}` : ""}.]`;
|
|
12352
|
+
const recover = opts.gathered ? "Partial work it gathered is below \u2014 write the files yourself from it, or send a tight write-only follow-up with message_subagent." : `Use read_subagent("${opts.subagentId}", mode:"full") to recover what it gathered.`;
|
|
12353
|
+
return opts.gathered ? `${head} ${recover}
|
|
12354
|
+
|
|
12355
|
+
${opts.gathered}` : `${head} ${recover}`;
|
|
12356
|
+
};
|
|
12343
12357
|
var AgentOrchestrator = class {
|
|
12344
12358
|
harness;
|
|
12345
12359
|
conversationStore;
|
|
@@ -12865,6 +12879,7 @@ var AgentOrchestrator = class {
|
|
|
12865
12879
|
const draft = createTurnDraftState();
|
|
12866
12880
|
let latestRunId = "";
|
|
12867
12881
|
let runResult;
|
|
12882
|
+
let runError;
|
|
12868
12883
|
try {
|
|
12869
12884
|
const conversation = await this.conversationStore.getWithArchive(childConversationId);
|
|
12870
12885
|
if (!conversation) throw new Error("Subagent conversation not found");
|
|
@@ -13001,6 +13016,7 @@ var AgentOrchestrator = class {
|
|
|
13001
13016
|
}
|
|
13002
13017
|
}
|
|
13003
13018
|
if (event.type === "run:error") {
|
|
13019
|
+
runError = { code: event.error.code, message: event.error.message };
|
|
13004
13020
|
draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
|
|
13005
13021
|
}
|
|
13006
13022
|
await this.eventSink(childConversationId, event);
|
|
@@ -13048,7 +13064,12 @@ var AgentOrchestrator = class {
|
|
|
13048
13064
|
}
|
|
13049
13065
|
return;
|
|
13050
13066
|
}
|
|
13051
|
-
|
|
13067
|
+
const abnormalEnd = !runResult;
|
|
13068
|
+
conv.subagentMeta = {
|
|
13069
|
+
...conv.subagentMeta,
|
|
13070
|
+
status: abnormalEnd ? "error" : "completed",
|
|
13071
|
+
...abnormalEnd ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {}
|
|
13072
|
+
};
|
|
13052
13073
|
await this.conversationStore.update(conv);
|
|
13053
13074
|
}
|
|
13054
13075
|
this.hooks?.onStreamEnd?.(childConversationId);
|
|
@@ -13057,18 +13078,25 @@ var AgentOrchestrator = class {
|
|
|
13057
13078
|
subagentId: childConversationId,
|
|
13058
13079
|
conversationId: childConversationId
|
|
13059
13080
|
});
|
|
13060
|
-
let
|
|
13061
|
-
if (!
|
|
13081
|
+
let gathered = realResponseText(runResult?.response) || realResponseText(draft.assistantResponse);
|
|
13082
|
+
if (!gathered) {
|
|
13062
13083
|
const freshSubConv = await this.conversationStore.get(childConversationId);
|
|
13063
|
-
if (freshSubConv)
|
|
13064
|
-
subagentResponse = lastAssistantText(freshSubConv.messages);
|
|
13065
|
-
}
|
|
13084
|
+
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
13066
13085
|
}
|
|
13086
|
+
const abnormal = !runResult;
|
|
13087
|
+
const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: childConversationId, gathered, runError }) : gathered;
|
|
13067
13088
|
const pendingResult = {
|
|
13068
13089
|
subagentId: childConversationId,
|
|
13069
13090
|
task,
|
|
13070
|
-
status: "completed",
|
|
13071
|
-
result:
|
|
13091
|
+
status: abnormal ? "error" : "completed",
|
|
13092
|
+
result: {
|
|
13093
|
+
status: runResult?.status ?? "error",
|
|
13094
|
+
response: subagentResponse,
|
|
13095
|
+
steps: runResult?.steps ?? 0,
|
|
13096
|
+
tokens: { input: 0, output: 0, cached: 0 },
|
|
13097
|
+
duration: runResult?.duration ?? 0
|
|
13098
|
+
},
|
|
13099
|
+
...abnormal ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {},
|
|
13072
13100
|
timestamp: Date.now()
|
|
13073
13101
|
};
|
|
13074
13102
|
await this.conversationStore.appendSubagentResult(parentConversationId, pendingResult);
|
|
@@ -13316,6 +13344,7 @@ ${resultBody}`,
|
|
|
13316
13344
|
this.activeSubagentRuns.set(conversationId, { abortController: childAbortController, harness: childHarness, parentConversationId });
|
|
13317
13345
|
const draft = createTurnDraftState();
|
|
13318
13346
|
let runResult;
|
|
13347
|
+
let runError;
|
|
13319
13348
|
try {
|
|
13320
13349
|
const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: conversationId }) ?? {};
|
|
13321
13350
|
for await (const event of childHarness.runWithTelemetry({
|
|
@@ -13348,6 +13377,7 @@ ${resultBody}`,
|
|
|
13348
13377
|
}
|
|
13349
13378
|
}
|
|
13350
13379
|
if (event.type === "run:error") {
|
|
13380
|
+
runError = { code: event.error.code, message: event.error.message };
|
|
13351
13381
|
draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
|
|
13352
13382
|
}
|
|
13353
13383
|
await this.eventSink(conversationId, event);
|
|
@@ -13396,7 +13426,12 @@ ${resultBody}`,
|
|
|
13396
13426
|
}
|
|
13397
13427
|
return;
|
|
13398
13428
|
}
|
|
13399
|
-
|
|
13429
|
+
const abnormalEnd = !runResult;
|
|
13430
|
+
conv.subagentMeta = {
|
|
13431
|
+
...conv.subagentMeta,
|
|
13432
|
+
status: abnormalEnd ? "error" : "completed",
|
|
13433
|
+
...abnormalEnd ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {}
|
|
13434
|
+
};
|
|
13400
13435
|
await this.conversationStore.update(conv);
|
|
13401
13436
|
}
|
|
13402
13437
|
this.activeSubagentRuns.delete(conversationId);
|
|
@@ -13405,20 +13440,21 @@ ${resultBody}`,
|
|
|
13405
13440
|
subagentId: conversationId,
|
|
13406
13441
|
conversationId
|
|
13407
13442
|
});
|
|
13408
|
-
let
|
|
13409
|
-
if (!
|
|
13443
|
+
let gathered = realResponseText(runResult?.response) || realResponseText(draft.assistantResponse);
|
|
13444
|
+
if (!gathered) {
|
|
13410
13445
|
const freshSubConv = await this.conversationStore.get(conversationId);
|
|
13411
|
-
if (freshSubConv)
|
|
13412
|
-
subagentResponse = lastAssistantText(freshSubConv.messages);
|
|
13413
|
-
}
|
|
13446
|
+
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
13414
13447
|
}
|
|
13448
|
+
const abnormal = !runResult;
|
|
13449
|
+
const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: conversationId, gathered, runError }) : gathered;
|
|
13415
13450
|
const parentConv = await this.conversationStore.get(parentConversationId);
|
|
13416
13451
|
if (parentConv) {
|
|
13417
13452
|
const result = {
|
|
13418
13453
|
subagentId: conversationId,
|
|
13419
13454
|
task,
|
|
13420
|
-
status: "completed",
|
|
13421
|
-
result: { status: "
|
|
13455
|
+
status: abnormal ? "error" : "completed",
|
|
13456
|
+
result: { status: runResult?.status ?? "error", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
|
|
13457
|
+
...abnormal ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {},
|
|
13422
13458
|
timestamp: Date.now()
|
|
13423
13459
|
};
|
|
13424
13460
|
await this.conversationStore.appendSubagentResult(parentConversationId, result);
|
|
@@ -14048,6 +14084,7 @@ export {
|
|
|
14048
14084
|
ToolDispatcher,
|
|
14049
14085
|
VFS_SCHEME,
|
|
14050
14086
|
VercelBlobUploadStore,
|
|
14087
|
+
abnormalEndResponse,
|
|
14051
14088
|
applyTurnMetadata,
|
|
14052
14089
|
buildAgentDirectoryName,
|
|
14053
14090
|
buildApprovalCheckpoints,
|
|
@@ -14122,6 +14159,7 @@ export {
|
|
|
14122
14159
|
ponchoDocsTool,
|
|
14123
14160
|
readOpenAICodexSession,
|
|
14124
14161
|
readSkillResource,
|
|
14162
|
+
realResponseText,
|
|
14125
14163
|
recordStandardTurnEvent,
|
|
14126
14164
|
renderAgentPrompt,
|
|
14127
14165
|
resolveAgentIdentity,
|
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -146,6 +146,14 @@ export interface HarnessOptions {
|
|
|
146
146
|
* should also be browsable in the VFS. Empty by default.
|
|
147
147
|
*/
|
|
148
148
|
systemSkillPaths?: string[];
|
|
149
|
+
/**
|
|
150
|
+
* Override the per-run hard wall-clock timeout, in seconds, taking
|
|
151
|
+
* precedence over the agent definition's `limits.timeout`. Platforms use
|
|
152
|
+
* this to give background subagents a longer budget than the foreground
|
|
153
|
+
* agent without forking the agent definition (e.g. a 1h research subagent
|
|
154
|
+
* vs. a 5-min foreground turn). `0` disables the hard timeout.
|
|
155
|
+
*/
|
|
156
|
+
runTimeoutSecOverride?: number;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
export interface HarnessRunOutput {
|
|
@@ -848,6 +856,7 @@ function extractMediaFromToolOutput(output: unknown): {
|
|
|
848
856
|
export class AgentHarness {
|
|
849
857
|
private readonly workingDir: string;
|
|
850
858
|
private readonly environment: HarnessOptions["environment"];
|
|
859
|
+
private readonly runTimeoutSecOverride?: number;
|
|
851
860
|
private modelProvider: ModelProviderFactory;
|
|
852
861
|
private readonly modelProviderInjected: boolean;
|
|
853
862
|
private readonly dispatcher = new ToolDispatcher();
|
|
@@ -1084,6 +1093,7 @@ export class AgentHarness {
|
|
|
1084
1093
|
constructor(options: HarnessOptions = {}) {
|
|
1085
1094
|
this.workingDir = options.workingDir ?? process.cwd();
|
|
1086
1095
|
this.environment = options.environment ?? "development";
|
|
1096
|
+
this.runTimeoutSecOverride = options.runTimeoutSecOverride;
|
|
1087
1097
|
this.modelProviderInjected = !!options.modelProvider;
|
|
1088
1098
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
1089
1099
|
this.uploadStore = options.uploadStore;
|
|
@@ -2126,7 +2136,9 @@ export class AgentHarness {
|
|
|
2126
2136
|
const runId = `run_${randomUUID()}`;
|
|
2127
2137
|
const start = now();
|
|
2128
2138
|
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 20;
|
|
2129
|
-
|
|
2139
|
+
// A constructor-level override (e.g. a longer budget for background
|
|
2140
|
+
// subagents) takes precedence over the agent definition's limits.timeout.
|
|
2141
|
+
const configuredTimeout = this.runTimeoutSecOverride ?? agent.frontmatter.limits?.timeout;
|
|
2130
2142
|
const timeoutMs = this.environment === "development" && configuredTimeout == null
|
|
2131
2143
|
? 0 // no hard timeout in development unless explicitly configured
|
|
2132
2144
|
: (configuredTimeout ?? 300) * 1000;
|
|
@@ -67,6 +67,39 @@ export const lastAssistantText = (messages: Message[]): string => {
|
|
|
67
67
|
return "";
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The run loop stuffs a synthetic `[Error: ...]` placeholder into the draft /
|
|
72
|
+
* persisted assistant text when a run ends on `run:error` (e.g. a timeout).
|
|
73
|
+
* That placeholder is not real model output — strip it so we don't surface it
|
|
74
|
+
* to the parent as the subagent's "response".
|
|
75
|
+
*/
|
|
76
|
+
export const realResponseText = (text: string | undefined): string => {
|
|
77
|
+
const t = (text ?? "").trim();
|
|
78
|
+
return t.startsWith("[Error:") ? "" : t;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the result text delivered to the parent when a subagent ended
|
|
83
|
+
* abnormally (timeout / error) with no RunResult. We never drop the work it
|
|
84
|
+
* gathered, and the parent is told it didn't finish — e.g. it may not have
|
|
85
|
+
* written its output files — plus how to recover (use what's here, send a
|
|
86
|
+
* write-only follow-up, or read the full transcript).
|
|
87
|
+
*/
|
|
88
|
+
export const abnormalEndResponse = (opts: {
|
|
89
|
+
subagentId: string;
|
|
90
|
+
gathered: string;
|
|
91
|
+
runError?: { code?: string; message?: string };
|
|
92
|
+
}): string => {
|
|
93
|
+
const timedOut = opts.runError?.code === "TIMEOUT";
|
|
94
|
+
const head = timedOut
|
|
95
|
+
? "[Subagent hit its time limit before finishing — it may not have written its output files.]"
|
|
96
|
+
: `[Subagent ended before finishing${opts.runError?.message ? `: ${opts.runError.message}` : ""}.]`;
|
|
97
|
+
const recover = opts.gathered
|
|
98
|
+
? "Partial work it gathered is below — write the files yourself from it, or send a tight write-only follow-up with message_subagent."
|
|
99
|
+
: `Use read_subagent("${opts.subagentId}", mode:"full") to recover what it gathered.`;
|
|
100
|
+
return opts.gathered ? `${head} ${recover}\n\n${opts.gathered}` : `${head} ${recover}`;
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
// ── Types ──
|
|
71
104
|
|
|
72
105
|
export type ActiveConversationRun = {
|
|
@@ -762,6 +795,7 @@ export class AgentOrchestrator {
|
|
|
762
795
|
const draft = createTurnDraftState();
|
|
763
796
|
let latestRunId = "";
|
|
764
797
|
let runResult: { status: "completed" | "error" | "cancelled"; response?: string; steps: number; duration: number; continuation?: boolean; continuationMessages?: Message[] } | undefined;
|
|
798
|
+
let runError: { code?: string; message?: string } | undefined;
|
|
765
799
|
|
|
766
800
|
try {
|
|
767
801
|
const conversation = await this.conversationStore.getWithArchive(childConversationId);
|
|
@@ -911,6 +945,7 @@ export class AgentOrchestrator {
|
|
|
911
945
|
}
|
|
912
946
|
}
|
|
913
947
|
if (event.type === "run:error") {
|
|
948
|
+
runError = { code: event.error.code, message: event.error.message };
|
|
914
949
|
draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
|
|
915
950
|
}
|
|
916
951
|
await this.eventSink(childConversationId, event);
|
|
@@ -961,7 +996,17 @@ export class AgentOrchestrator {
|
|
|
961
996
|
return;
|
|
962
997
|
}
|
|
963
998
|
|
|
964
|
-
|
|
999
|
+
// No runResult means the run ended on run:error (timeout / model
|
|
1000
|
+
// error) rather than run:completed — flag the subagent accordingly
|
|
1001
|
+
// instead of faking "completed".
|
|
1002
|
+
const abnormalEnd = !runResult;
|
|
1003
|
+
conv.subagentMeta = {
|
|
1004
|
+
...conv.subagentMeta!,
|
|
1005
|
+
status: abnormalEnd ? "error" : "completed",
|
|
1006
|
+
...(abnormalEnd
|
|
1007
|
+
? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } }
|
|
1008
|
+
: {}),
|
|
1009
|
+
};
|
|
965
1010
|
await this.conversationStore.update(conv);
|
|
966
1011
|
}
|
|
967
1012
|
|
|
@@ -972,18 +1017,36 @@ export class AgentOrchestrator {
|
|
|
972
1017
|
conversationId: childConversationId,
|
|
973
1018
|
});
|
|
974
1019
|
|
|
975
|
-
|
|
976
|
-
|
|
1020
|
+
// Recover the subagent's real output: prefer the run response, then the
|
|
1021
|
+
// streamed draft, then walk the transcript — discarding the synthetic
|
|
1022
|
+
// "[Error: ...]" placeholder at each step.
|
|
1023
|
+
let gathered = realResponseText(runResult?.response) || realResponseText(draft.assistantResponse);
|
|
1024
|
+
if (!gathered) {
|
|
977
1025
|
const freshSubConv = await this.conversationStore.get(childConversationId);
|
|
978
|
-
if (freshSubConv)
|
|
979
|
-
subagentResponse = lastAssistantText(freshSubConv.messages);
|
|
980
|
-
}
|
|
1026
|
+
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
981
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
// On an abnormal end (timeout / error) there is no runResult; don't drop
|
|
1030
|
+
// the work — deliver what it gathered, tagged so the parent knows it
|
|
1031
|
+
// didn't finish, and build a result so it never renders as "(no result)".
|
|
1032
|
+
const abnormal = !runResult;
|
|
1033
|
+
const subagentResponse = abnormal
|
|
1034
|
+
? abnormalEndResponse({ subagentId: childConversationId, gathered, runError })
|
|
1035
|
+
: gathered;
|
|
982
1036
|
const pendingResult: PendingSubagentResult = {
|
|
983
1037
|
subagentId: childConversationId,
|
|
984
1038
|
task,
|
|
985
|
-
status: "completed",
|
|
986
|
-
result:
|
|
1039
|
+
status: abnormal ? "error" : "completed",
|
|
1040
|
+
result: {
|
|
1041
|
+
status: runResult?.status ?? "error",
|
|
1042
|
+
response: subagentResponse,
|
|
1043
|
+
steps: runResult?.steps ?? 0,
|
|
1044
|
+
tokens: { input: 0, output: 0, cached: 0 },
|
|
1045
|
+
duration: runResult?.duration ?? 0,
|
|
1046
|
+
},
|
|
1047
|
+
...(abnormal
|
|
1048
|
+
? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } }
|
|
1049
|
+
: {}),
|
|
987
1050
|
timestamp: Date.now(),
|
|
988
1051
|
};
|
|
989
1052
|
await this.conversationStore.appendSubagentResult(parentConversationId, pendingResult);
|
|
@@ -1271,7 +1334,8 @@ export class AgentOrchestrator {
|
|
|
1271
1334
|
this.activeSubagentRuns.set(conversationId, { abortController: childAbortController, harness: childHarness, parentConversationId });
|
|
1272
1335
|
|
|
1273
1336
|
const draft = createTurnDraftState();
|
|
1274
|
-
let runResult: { status:
|
|
1337
|
+
let runResult: { status: "completed" | "error" | "cancelled"; response?: string; steps: number; duration: number; continuation?: boolean; continuationMessages?: Message[] } | undefined;
|
|
1338
|
+
let runError: { code?: string; message?: string } | undefined;
|
|
1275
1339
|
|
|
1276
1340
|
try {
|
|
1277
1341
|
const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: conversationId }) ?? {};
|
|
@@ -1306,6 +1370,7 @@ export class AgentOrchestrator {
|
|
|
1306
1370
|
}
|
|
1307
1371
|
}
|
|
1308
1372
|
if (event.type === "run:error") {
|
|
1373
|
+
runError = { code: event.error.code, message: event.error.message };
|
|
1309
1374
|
draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
|
|
1310
1375
|
}
|
|
1311
1376
|
await this.eventSink(conversationId, event);
|
|
@@ -1355,7 +1420,14 @@ export class AgentOrchestrator {
|
|
|
1355
1420
|
return;
|
|
1356
1421
|
}
|
|
1357
1422
|
|
|
1358
|
-
|
|
1423
|
+
const abnormalEnd = !runResult;
|
|
1424
|
+
conv.subagentMeta = {
|
|
1425
|
+
...conv.subagentMeta!,
|
|
1426
|
+
status: abnormalEnd ? "error" : "completed",
|
|
1427
|
+
...(abnormalEnd
|
|
1428
|
+
? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } }
|
|
1429
|
+
: {}),
|
|
1430
|
+
};
|
|
1359
1431
|
await this.conversationStore.update(conv);
|
|
1360
1432
|
}
|
|
1361
1433
|
|
|
@@ -1366,21 +1438,26 @@ export class AgentOrchestrator {
|
|
|
1366
1438
|
conversationId,
|
|
1367
1439
|
});
|
|
1368
1440
|
|
|
1369
|
-
let
|
|
1370
|
-
if (!
|
|
1441
|
+
let gathered = realResponseText(runResult?.response) || realResponseText(draft.assistantResponse);
|
|
1442
|
+
if (!gathered) {
|
|
1371
1443
|
const freshSubConv = await this.conversationStore.get(conversationId);
|
|
1372
|
-
if (freshSubConv)
|
|
1373
|
-
subagentResponse = lastAssistantText(freshSubConv.messages);
|
|
1374
|
-
}
|
|
1444
|
+
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
1375
1445
|
}
|
|
1446
|
+
const abnormal = !runResult;
|
|
1447
|
+
const subagentResponse = abnormal
|
|
1448
|
+
? abnormalEndResponse({ subagentId: conversationId, gathered, runError })
|
|
1449
|
+
: gathered;
|
|
1376
1450
|
|
|
1377
1451
|
const parentConv = await this.conversationStore.get(parentConversationId);
|
|
1378
1452
|
if (parentConv) {
|
|
1379
1453
|
const result: PendingSubagentResult = {
|
|
1380
1454
|
subagentId: conversationId,
|
|
1381
1455
|
task,
|
|
1382
|
-
status: "completed",
|
|
1383
|
-
result: { status: "
|
|
1456
|
+
status: abnormal ? "error" : "completed",
|
|
1457
|
+
result: { status: runResult?.status ?? "error", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
|
|
1458
|
+
...(abnormal
|
|
1459
|
+
? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } }
|
|
1460
|
+
: {}),
|
|
1384
1461
|
timestamp: Date.now(),
|
|
1385
1462
|
};
|
|
1386
1463
|
await this.conversationStore.appendSubagentResult(parentConversationId, result);
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
recordStandardTurnEvent,
|
|
10
10
|
executeConversationTurn,
|
|
11
11
|
lastAssistantText,
|
|
12
|
+
realResponseText,
|
|
13
|
+
abnormalEndResponse,
|
|
12
14
|
} from "../src/orchestrator/index.js";
|
|
13
15
|
import type { Conversation } from "../src/state.js";
|
|
14
16
|
|
|
@@ -237,3 +239,50 @@ describe("lastAssistantText (subagent result extraction)", () => {
|
|
|
237
239
|
expect(lastAssistantText(messages)).toBe("");
|
|
238
240
|
});
|
|
239
241
|
});
|
|
242
|
+
|
|
243
|
+
describe("realResponseText (strips run:error placeholder)", () => {
|
|
244
|
+
it("drops the synthetic [Error: ...] placeholder", () => {
|
|
245
|
+
expect(realResponseText("[Error: Run exceeded timeout of 300s]")).toBe("");
|
|
246
|
+
});
|
|
247
|
+
it("keeps real text and trims", () => {
|
|
248
|
+
expect(realResponseText(" done, wrote the file ")).toBe("done, wrote the file");
|
|
249
|
+
});
|
|
250
|
+
it("handles undefined", () => {
|
|
251
|
+
expect(realResponseText(undefined)).toBe("");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("abnormalEndResponse (graceful timeout / error delivery)", () => {
|
|
256
|
+
it("timeout WITH gathered work: notes the cutoff and includes the work", () => {
|
|
257
|
+
const out = abnormalEndResponse({
|
|
258
|
+
subagentId: "sub_1",
|
|
259
|
+
gathered: "Found 12 competitors: A, B, C...",
|
|
260
|
+
runError: { code: "TIMEOUT", message: "Run exceeded timeout of 3600s" },
|
|
261
|
+
});
|
|
262
|
+
expect(out).toContain("time limit");
|
|
263
|
+
expect(out).toContain("may not have written its output files");
|
|
264
|
+
expect(out).toContain("Found 12 competitors: A, B, C...");
|
|
265
|
+
expect(out).not.toContain("(no result)");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("timeout WITHOUT gathered work: points at read_subagent to recover", () => {
|
|
269
|
+
const out = abnormalEndResponse({
|
|
270
|
+
subagentId: "sub_2",
|
|
271
|
+
gathered: "",
|
|
272
|
+
runError: { code: "TIMEOUT", message: "Run exceeded timeout of 3600s" },
|
|
273
|
+
});
|
|
274
|
+
expect(out).toContain("time limit");
|
|
275
|
+
expect(out).toContain('read_subagent("sub_2"');
|
|
276
|
+
expect(out).toContain('mode:"full"');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("non-timeout error: surfaces the error message", () => {
|
|
280
|
+
const out = abnormalEndResponse({
|
|
281
|
+
subagentId: "sub_3",
|
|
282
|
+
gathered: "",
|
|
283
|
+
runError: { code: "EMPTY_RESPONSE", message: "model returned no content" },
|
|
284
|
+
});
|
|
285
|
+
expect(out).toContain("ended before finishing");
|
|
286
|
+
expect(out).toContain("model returned no content");
|
|
287
|
+
});
|
|
288
|
+
});
|