@poncho-ai/harness 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.26.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.27.0 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,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 276.38 KB
12
- ESM ⚡️ Build success in 147ms
11
+ ESM dist/index.js 278.61 KB
12
+ ESM ⚡️ Build success in 148ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 7090ms
15
- DTS dist/index.d.ts 28.59 KB
14
+ DTS ⚡️ Build success in 7054ms
15
+ DTS dist/index.d.ts 29.13 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#42](https://github.com/cesr/poncho-ai/pull/42) [`e58a984`](https://github.com/cesr/poncho-ai/commit/e58a984efaa673b649318102bbf735fb4c2f9172) Thanks [@cesr](https://github.com/cesr)! - Add continuation model and fire-and-forget subagents
8
+
9
+ **Continuation model**: Agents no longer send a synthetic `"Continue"` user message between steps. Instead, the harness injects a transient signal when needed, and the full internal message chain is preserved across continuations so the LLM never loses context. `RunInput` gains `disableSoftDeadline` and `RunResult` gains `continuationMessages`.
10
+
11
+ **Fire-and-forget subagents**: Subagents now run asynchronously in the background. `spawn_subagent` returns immediately with a subagent ID; results are delivered back to the parent conversation as a callback once the subagent completes. Subagents cannot spawn their own subagents. The web UI shows results in a collapsible disclosure and reconnects the live event stream automatically when the parent agent resumes.
12
+
13
+ **Bug fixes**:
14
+ - Fixed a race condition where concurrent runs on the same harness instance could assign a subagent or browser tab to the wrong parent conversation (shared `_currentRunConversationId` field replaced with per-run `ToolContext.conversationId`).
15
+ - Fixed Upstash KV store silently dropping large values by switching from URL-path encoding to request body format for `SET`/`SETEX` commands.
16
+ - Fixed empty assistant content blocks causing Anthropic `text content blocks must be non-empty` errors.
17
+
18
+ **Client**: Added `getConversationStatus()` and `waitForSubagents` option on `sendMessage()`.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [[`e58a984`](https://github.com/cesr/poncho-ai/commit/e58a984efaa673b649318102bbf735fb4c2f9172)]:
23
+ - @poncho-ai/sdk@1.6.0
24
+
3
25
  ## 0.26.0
4
26
 
5
27
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -123,6 +123,14 @@ interface StateStore {
123
123
  set(state: ConversationState): Promise<void>;
124
124
  delete(runId: string): Promise<void>;
125
125
  }
126
+ interface PendingSubagentResult {
127
+ subagentId: string;
128
+ task: string;
129
+ status: "completed" | "error" | "stopped";
130
+ result?: _poncho_ai_sdk.RunResult;
131
+ error?: _poncho_ai_sdk.AgentFailure;
132
+ timestamp: number;
133
+ }
126
134
  interface Conversation {
127
135
  conversationId: string;
128
136
  title: string;
@@ -161,6 +169,13 @@ interface Conversation {
161
169
  channelId: string;
162
170
  platformThreadId: string;
163
171
  };
172
+ pendingSubagentResults?: PendingSubagentResult[];
173
+ subagentCallbackCount?: number;
174
+ runningCallbackSince?: number;
175
+ lastActivityAt?: number;
176
+ /** Harness-internal message chain preserved across continuation runs.
177
+ * Cleared when a run completes without continuation. */
178
+ _continuationMessages?: Message[];
164
179
  createdAt: number;
165
180
  updatedAt: number;
166
181
  }
@@ -172,6 +187,7 @@ interface ConversationStore {
172
187
  update(conversation: Conversation): Promise<void>;
173
188
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
174
189
  delete(conversationId: string): Promise<boolean>;
190
+ appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
175
191
  }
176
192
  type StateProviderName = "local" | "memory" | "redis" | "upstash" | "dynamodb";
177
193
  interface StateConfig {
@@ -204,6 +220,7 @@ declare class InMemoryConversationStore implements ConversationStore {
204
220
  update(conversation: Conversation): Promise<void>;
205
221
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
206
222
  delete(conversationId: string): Promise<boolean>;
223
+ appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
207
224
  }
208
225
  type ConversationSummary = {
209
226
  conversationId: string;
@@ -530,13 +547,16 @@ interface SubagentSummary {
530
547
  status: string;
531
548
  messageCount: number;
532
549
  }
550
+ interface SubagentSpawnResult {
551
+ subagentId: string;
552
+ }
533
553
  interface SubagentManager {
534
554
  spawn(opts: {
535
555
  task: string;
536
556
  parentConversationId: string;
537
557
  ownerId: string;
538
- }): Promise<SubagentResult>;
539
- sendMessage(subagentId: string, message: string): Promise<SubagentResult>;
558
+ }): Promise<SubagentSpawnResult>;
559
+ sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
540
560
  stop(subagentId: string): Promise<void>;
541
561
  list(parentConversationId: string): Promise<SubagentSummary[]>;
542
562
  }
@@ -653,10 +673,6 @@ declare class AgentHarness {
653
673
  initialize(): Promise<void>;
654
674
  private buildBrowserStoragePersistence;
655
675
  private initBrowserTools;
656
- /** Conversation ID of the currently executing run (set during run, cleared after). */
657
- private _currentRunConversationId?;
658
- /** Owner ID of the currently executing run (used by subagent tools). */
659
- private _currentRunOwnerId?;
660
676
  get browserSession(): unknown;
661
677
  shutdown(): Promise<void>;
662
678
  listTools(): ToolDefinition[];
@@ -795,6 +811,6 @@ declare class TelemetryEmitter {
795
811
  private sendOtlp;
796
812
  }
797
813
 
798
- declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
814
+ declare const createSubagentTools: (manager: SubagentManager) => ToolDefinition[];
799
815
 
800
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
816
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentResult, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -617,8 +617,10 @@ Response: Server-Sent Events (\`run:started\`, \`model:chunk\`, \`tool:*\`, \`ru
617
617
 
618
618
  On serverless deployments with \`PONCHO_MAX_DURATION\` set, the \`run:completed\` event may
619
619
  include \`continuation: true\` in \`result\`, indicating the agent stopped early due to a
620
- platform timeout and the client should send another message (e.g., \`"Continue"\`) on the
621
- same conversation to resume.
620
+ platform timeout. The server preserves the full internal message chain so the agent
621
+ resumes with complete context. The web UI and client SDK handle continuation automatically
622
+ by re-posting to the same conversation with \`{ continuation: true }\` \u2014 no manual
623
+ "Continue" message is needed.
622
624
 
623
625
  ## Build a custom chat UI
624
626
 
@@ -1288,28 +1290,28 @@ When \`@sparticuz/chromium\` is installed and a serverless environment is detect
1288
1290
 
1289
1291
  ## Subagents
1290
1292
 
1291
- Poncho agents can spawn recursive copies of themselves as **subagents**. Each subagent runs in its own independent conversation with full access to the agent's tools and skills. The parent agent controls the subagent lifecycle and receives results directly.
1293
+ Poncho agents can spawn **subagents** \u2014 independent background tasks that run in their own conversations. Each subagent has full access to the agent's tools and skills. Subagents run asynchronously and their results are delivered back to the parent automatically.
1292
1294
 
1293
1295
  Subagents are useful when an agent needs to parallelize work, delegate a subtask, or isolate a line of investigation without polluting the main conversation context.
1294
1296
 
1295
1297
  ### How it works
1296
1298
 
1297
- When the agent decides to use a subagent, it calls \`spawn_subagent\` with a task description. The subagent runs to completion and the result is returned to the parent \u2014 the call is **blocking**, so the parent waits for the subagent to finish before continuing.
1299
+ When the agent decides to use a subagent, it calls \`spawn_subagent\` with a task description. The tool returns immediately with a subagent ID and \`status: "running"\`. The subagent runs in the background and, when it completes, its result is delivered to the parent conversation as a message \u2014 triggering a callback that lets the parent process or summarize the result.
1298
1300
 
1299
- The parent can also send follow-up messages to existing subagents with \`message_subagent\`, stop a running subagent with \`stop_subagent\`, or list all its subagents with \`list_subagents\`.
1301
+ The agent can spawn multiple subagents in a single response and they run concurrently. The parent can also send follow-up messages to existing subagents with \`message_subagent\`, stop a running subagent with \`stop_subagent\`, or list all its subagents with \`list_subagents\`.
1300
1302
 
1301
1303
  ### Available tools
1302
1304
 
1303
1305
  | Tool | Description |
1304
1306
  |------|-------------|
1305
- | \`spawn_subagent\` | Create a new subagent with a task. Blocks until the subagent completes and returns the result. |
1306
- | \`message_subagent\` | Send a follow-up message to an existing subagent. Blocks until it responds. |
1307
+ | \`spawn_subagent\` | Create a new subagent with a task. Returns immediately; results are delivered asynchronously. |
1308
+ | \`message_subagent\` | Send a follow-up message to an existing subagent. Returns immediately. |
1307
1309
  | \`stop_subagent\` | Stop a running subagent. |
1308
1310
  | \`list_subagents\` | List all subagents for the current conversation with their IDs, tasks, and statuses. |
1309
1311
 
1310
1312
  ### Limits
1311
1313
 
1312
- - **Max depth**: 3 levels of nesting (an agent can spawn a subagent, which can spawn another, but no deeper).
1314
+ - **No nesting**: subagents cannot spawn their own subagents.
1313
1315
  - **Max concurrent**: 5 subagents per parent conversation.
1314
1316
 
1315
1317
  ### Memory isolation
@@ -2032,7 +2034,7 @@ var ponchoDocsTool = defineTool({
2032
2034
  import { randomUUID as randomUUID3 } from "crypto";
2033
2035
  import { readFile as readFile8 } from "fs/promises";
2034
2036
  import { resolve as resolve10 } from "path";
2035
- import { getTextContent as getTextContent3 } from "@poncho-ai/sdk";
2037
+ import { getTextContent as getTextContent2 } from "@poncho-ai/sdk";
2036
2038
 
2037
2039
  // src/upload-store.ts
2038
2040
  import { createHash as createHash2 } from "crypto";
@@ -2368,16 +2370,26 @@ var UpstashKVStore = class {
2368
2370
  return payload.result ?? void 0;
2369
2371
  }
2370
2372
  async set(key, value) {
2371
- await fetch(
2372
- `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
2373
- { method: "POST", headers: this.headers() }
2374
- );
2373
+ const response = await fetch(this.baseUrl, {
2374
+ method: "POST",
2375
+ headers: this.headers(),
2376
+ body: JSON.stringify(["SET", key, value])
2377
+ });
2378
+ if (!response.ok) {
2379
+ const text = await response.text().catch(() => "");
2380
+ console.error(`[kv][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
2381
+ }
2375
2382
  }
2376
2383
  async setWithTtl(key, value, ttl) {
2377
- await fetch(
2378
- `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(value)}`,
2379
- { method: "POST", headers: this.headers() }
2380
- );
2384
+ const response = await fetch(this.baseUrl, {
2385
+ method: "POST",
2386
+ headers: this.headers(),
2387
+ body: JSON.stringify(["SETEX", key, Math.max(1, ttl), value])
2388
+ });
2389
+ if (!response.ok) {
2390
+ const text = await response.text().catch(() => "");
2391
+ console.error(`[kv][upstash] SETEX failed (${response.status}): ${text.slice(0, 200)}`);
2392
+ }
2381
2393
  }
2382
2394
  };
2383
2395
  var RedisKVStore = class {
@@ -4247,36 +4259,11 @@ var extractRunnableFunction = (value) => {
4247
4259
  };
4248
4260
 
4249
4261
  // src/subagent-tools.ts
4250
- import { defineTool as defineTool5, getTextContent as getTextContent2 } from "@poncho-ai/sdk";
4251
- var LAST_MESSAGES_TO_RETURN = 10;
4252
- var summarizeResult = (r) => {
4253
- const summary = {
4254
- subagentId: r.subagentId,
4255
- status: r.status
4256
- };
4257
- if (r.result) {
4258
- summary.result = {
4259
- status: r.result.status,
4260
- response: r.result.response,
4261
- steps: r.result.steps,
4262
- duration: r.result.duration
4263
- };
4264
- }
4265
- if (r.error) {
4266
- summary.error = r.error;
4267
- }
4268
- if (r.latestMessages && r.latestMessages.length > 0) {
4269
- summary.latestMessages = r.latestMessages.slice(-LAST_MESSAGES_TO_RETURN).map((m) => ({
4270
- role: m.role,
4271
- content: getTextContent2(m).slice(0, 2e3)
4272
- }));
4273
- }
4274
- return summary;
4275
- };
4276
- var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4262
+ import { defineTool as defineTool5 } from "@poncho-ai/sdk";
4263
+ var createSubagentTools = (manager) => [
4277
4264
  defineTool5({
4278
4265
  name: "spawn_subagent",
4279
- description: "Spawn a subagent to work on a task and wait for it to finish. The subagent is a full copy of yourself running in its own conversation context with access to the same tools (except memory writes). This call blocks until the subagent completes and returns its result.\n\nGuidelines:\n- Use subagents to parallelize work: call spawn_subagent multiple times in one response for independent sub-tasks -- they run concurrently.\n- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
4266
+ description: "Spawn a subagent to work on a task in the background. Returns immediately with a subagent ID. The subagent runs independently and its result will be delivered to you as a message in the conversation when it completes.\n\nGuidelines:\n- Spawn all needed subagents in a SINGLE response (they run concurrently), then end your turn with a brief message to the user.\n- Do NOT spawn more subagents in follow-up steps. Wait for results to be delivered before deciding if more work is needed.\n- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
4280
4267
  inputSchema: {
4281
4268
  type: "object",
4282
4269
  properties: {
@@ -4288,26 +4275,27 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4288
4275
  required: ["task"],
4289
4276
  additionalProperties: false
4290
4277
  },
4291
- handler: async (input) => {
4278
+ handler: async (input, context) => {
4292
4279
  const task = typeof input.task === "string" ? input.task : "";
4293
4280
  if (!task.trim()) {
4294
4281
  return { error: "task is required" };
4295
4282
  }
4296
- const conversationId = getConversationId();
4283
+ const conversationId = context.conversationId;
4297
4284
  if (!conversationId) {
4298
4285
  return { error: "no active conversation to spawn subagent from" };
4299
4286
  }
4300
- const result = await manager.spawn({
4287
+ const ownerId = typeof context.parameters.__ownerId === "string" ? context.parameters.__ownerId : "anonymous";
4288
+ const { subagentId } = await manager.spawn({
4301
4289
  task: task.trim(),
4302
4290
  parentConversationId: conversationId,
4303
- ownerId: getOwnerId()
4291
+ ownerId
4304
4292
  });
4305
- return summarizeResult(result);
4293
+ return { subagentId, status: "running" };
4306
4294
  }
4307
4295
  }),
4308
4296
  defineTool5({
4309
4297
  name: "message_subagent",
4310
- description: "Send a follow-up message to a completed or stopped subagent and wait for it to finish. This restarts the subagent with the new message and blocks until it completes. Only works when the subagent is not currently running.",
4298
+ description: "Send a follow-up message to a completed or stopped subagent. The subagent restarts in the background and its result will be delivered to you as a message when it completes. Only works when the subagent is not currently running.",
4311
4299
  inputSchema: {
4312
4300
  type: "object",
4313
4301
  properties: {
@@ -4329,8 +4317,8 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4329
4317
  if (!subagentId || !message.trim()) {
4330
4318
  return { error: "subagent_id and message are required" };
4331
4319
  }
4332
- const result = await manager.sendMessage(subagentId, message.trim());
4333
- return summarizeResult(result);
4320
+ const { subagentId: id } = await manager.sendMessage(subagentId, message.trim());
4321
+ return { subagentId: id, status: "running" };
4334
4322
  }
4335
4323
  }),
4336
4324
  defineTool5({
@@ -4364,8 +4352,8 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4364
4352
  properties: {},
4365
4353
  additionalProperties: false
4366
4354
  },
4367
- handler: async () => {
4368
- const conversationId = getConversationId();
4355
+ handler: async (_input, context) => {
4356
+ const conversationId = context.conversationId;
4369
4357
  if (!conversationId) {
4370
4358
  return { error: "no active conversation" };
4371
4359
  }
@@ -4961,11 +4949,7 @@ var AgentHarness = class _AgentHarness {
4961
4949
  setSubagentManager(manager) {
4962
4950
  this.subagentManager = manager;
4963
4951
  this.dispatcher.registerMany(
4964
- createSubagentTools(
4965
- manager,
4966
- () => this._currentRunConversationId,
4967
- () => this._currentRunOwnerId ?? "anonymous"
4968
- )
4952
+ createSubagentTools(manager)
4969
4953
  );
4970
4954
  }
4971
4955
  registerConfiguredBuiltInTools(config) {
@@ -5471,8 +5455,7 @@ var AgentHarness = class _AgentHarness {
5471
5455
  const session = new browserMod.BrowserSession(sessionId, browserCfg);
5472
5456
  this._browserSession = session;
5473
5457
  const tools = browserMod.createBrowserTools(
5474
- () => session,
5475
- () => this._currentRunConversationId ?? "__default__"
5458
+ () => session
5476
5459
  );
5477
5460
  for (const tool of tools) {
5478
5461
  if (this.isToolEnabled(tool.name)) {
@@ -5480,10 +5463,6 @@ var AgentHarness = class _AgentHarness {
5480
5463
  }
5481
5464
  }
5482
5465
  }
5483
- /** Conversation ID of the currently executing run (set during run, cleared after). */
5484
- _currentRunConversationId;
5485
- /** Owner ID of the currently executing run (used by subagent tools). */
5486
- _currentRunOwnerId;
5487
5466
  get browserSession() {
5488
5467
  return this._browserSession;
5489
5468
  }
@@ -5598,11 +5577,6 @@ var AgentHarness = class _AgentHarness {
5598
5577
  const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
5599
5578
  await this.refreshAgentIfChanged();
5600
5579
  await this.refreshSkillsIfChanged();
5601
- this._currentRunConversationId = input.conversationId;
5602
- const ownerParam = input.parameters?.__ownerId;
5603
- if (typeof ownerParam === "string") {
5604
- this._currentRunOwnerId = ownerParam;
5605
- }
5606
5580
  let agent = this.parsedAgent;
5607
5581
  const runId = `run_${randomUUID3()}`;
5608
5582
  const start = now();
@@ -5610,7 +5584,7 @@ var AgentHarness = class _AgentHarness {
5610
5584
  const configuredTimeout = agent.frontmatter.limits?.timeout;
5611
5585
  const timeoutMs = this.environment === "development" && configuredTimeout == null ? 0 : (configuredTimeout ?? 300) * 1e3;
5612
5586
  const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
5613
- const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
5587
+ const softDeadlineMs = input.disableSoftDeadline || platformMaxDurationSec <= 0 ? 0 : platformMaxDurationSec * 800;
5614
5588
  const messages = [...input.messages ?? []];
5615
5589
  const inputMessageCount = messages.length;
5616
5590
  const events = [];
@@ -5744,6 +5718,15 @@ ${this.skillFingerprint}`;
5744
5718
  metadata: { timestamp: now(), id: randomUUID3() }
5745
5719
  });
5746
5720
  }
5721
+ } else {
5722
+ const lastMsg = messages[messages.length - 1];
5723
+ if (lastMsg && lastMsg.role !== "user") {
5724
+ messages.push({
5725
+ role: "user",
5726
+ content: "[System: Your previous turn was interrupted by a time limit. Continue from where you left off \u2014 do NOT repeat what you already said. Proceed directly with the next action or tool call.]",
5727
+ metadata: { timestamp: now(), id: randomUUID3() }
5728
+ });
5729
+ }
5747
5730
  }
5748
5731
  let responseText = "";
5749
5732
  let totalInputTokens = 0;
@@ -5778,6 +5761,7 @@ ${this.skillFingerprint}`;
5778
5761
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
5779
5762
  duration: now() - start,
5780
5763
  continuation: true,
5764
+ continuationMessages: [...messages],
5781
5765
  maxSteps
5782
5766
  };
5783
5767
  yield pushEvent({ type: "run:completed", runId, result: result2 });
@@ -5811,7 +5795,7 @@ ${this.skillFingerprint}`;
5811
5795
  if (rich && rich.length > 0) {
5812
5796
  return [{ role: "tool", content: rich }];
5813
5797
  }
5814
- const textContent = typeof msg.content === "string" ? msg.content : getTextContent3(msg);
5798
+ const textContent = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
5815
5799
  try {
5816
5800
  const parsed = JSON.parse(textContent);
5817
5801
  if (!Array.isArray(parsed)) {
@@ -5861,7 +5845,7 @@ ${this.skillFingerprint}`;
5861
5845
  }
5862
5846
  }
5863
5847
  if (msg.role === "assistant") {
5864
- const assistantText = typeof msg.content === "string" ? msg.content : getTextContent3(msg);
5848
+ const assistantText = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
5865
5849
  try {
5866
5850
  const parsed = JSON.parse(assistantText);
5867
5851
  if (typeof parsed === "object" && parsed !== null) {
@@ -5895,12 +5879,15 @@ ${this.skillFingerprint}`;
5895
5879
  }
5896
5880
  } catch {
5897
5881
  }
5882
+ if (!assistantText || assistantText.trim().length === 0) {
5883
+ return [];
5884
+ }
5898
5885
  return [{ role: "assistant", content: assistantText }];
5899
5886
  }
5900
5887
  if (msg.role === "system") {
5901
5888
  return [{
5902
5889
  role: "system",
5903
- content: typeof msg.content === "string" ? msg.content : getTextContent3(msg)
5890
+ content: typeof msg.content === "string" ? msg.content : getTextContent2(msg)
5904
5891
  }];
5905
5892
  }
5906
5893
  if (msg.role === "user") {
@@ -6454,6 +6441,7 @@ ${this.skillFingerprint}`;
6454
6441
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6455
6442
  duration: now() - start,
6456
6443
  continuation: true,
6444
+ continuationMessages: [...messages],
6457
6445
  maxSteps
6458
6446
  };
6459
6447
  yield pushEvent({ type: "run:completed", runId, result });
@@ -6775,6 +6763,13 @@ var InMemoryConversationStore = class {
6775
6763
  async delete(conversationId) {
6776
6764
  return this.conversations.delete(conversationId);
6777
6765
  }
6766
+ async appendSubagentResult(conversationId, result) {
6767
+ const conversation = this.conversations.get(conversationId);
6768
+ if (!conversation) return;
6769
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
6770
+ conversation.pendingSubagentResults.push(result);
6771
+ conversation.updatedAt = Date.now();
6772
+ }
6778
6773
  };
6779
6774
  var FileConversationStore = class {
6780
6775
  workingDir;
@@ -6988,6 +6983,15 @@ var FileConversationStore = class {
6988
6983
  }
6989
6984
  return removed;
6990
6985
  }
6986
+ async appendSubagentResult(conversationId, result) {
6987
+ await this.ensureLoaded();
6988
+ const conversation = await this.get(conversationId);
6989
+ if (!conversation) return;
6990
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
6991
+ conversation.pendingSubagentResults.push(result);
6992
+ conversation.updatedAt = Date.now();
6993
+ await this.update(conversation);
6994
+ }
6991
6995
  };
6992
6996
  var FileStateStore = class {
6993
6997
  workingDir;
@@ -7067,6 +7071,7 @@ var KeyValueConversationStoreBase = class {
7067
7071
  ttl;
7068
7072
  agentIdPromise;
7069
7073
  ownerLocks = /* @__PURE__ */ new Map();
7074
+ appendLocks = /* @__PURE__ */ new Map();
7070
7075
  memoryFallback;
7071
7076
  constructor(ttl, workingDir, agentId) {
7072
7077
  this.ttl = ttl;
@@ -7085,6 +7090,18 @@ var KeyValueConversationStoreBase = class {
7085
7090
  }
7086
7091
  }
7087
7092
  }
7093
+ async withAppendLock(conversationId, task) {
7094
+ const prev = this.appendLocks.get(conversationId) ?? Promise.resolve();
7095
+ const next = prev.then(task, task);
7096
+ this.appendLocks.set(conversationId, next);
7097
+ try {
7098
+ await next;
7099
+ } finally {
7100
+ if (this.appendLocks.get(conversationId) === next) {
7101
+ this.appendLocks.delete(conversationId);
7102
+ }
7103
+ }
7104
+ }
7088
7105
  async namespace() {
7089
7106
  const agentId = await this.agentIdPromise;
7090
7107
  return `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
@@ -7305,6 +7322,16 @@ var KeyValueConversationStoreBase = class {
7305
7322
  });
7306
7323
  return true;
7307
7324
  }
7325
+ async appendSubagentResult(conversationId, result) {
7326
+ await this.withAppendLock(conversationId, async () => {
7327
+ const conversation = await this.get(conversationId);
7328
+ if (!conversation) return;
7329
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
7330
+ conversation.pendingSubagentResults.push(result);
7331
+ conversation.updatedAt = Date.now();
7332
+ await this.update(conversation);
7333
+ });
7334
+ }
7308
7335
  };
7309
7336
  var UpstashConversationStore = class extends KeyValueConversationStoreBase {
7310
7337
  baseUrl;
@@ -7347,20 +7374,26 @@ var UpstashConversationStore = class extends KeyValueConversationStoreBase {
7347
7374
  return (payload.result ?? []).map((v) => v ?? void 0);
7348
7375
  },
7349
7376
  set: async (key, value, ttl) => {
7350
- const endpoint = typeof ttl === "number" ? `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(
7351
- 1,
7352
- ttl
7353
- )}/${encodeURIComponent(value)}` : `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`;
7354
- await fetch(endpoint, {
7377
+ const command = typeof ttl === "number" ? ["SETEX", key, Math.max(1, ttl), value] : ["SET", key, value];
7378
+ const response = await fetch(this.baseUrl, {
7355
7379
  method: "POST",
7356
- headers: this.headers()
7380
+ headers: this.headers(),
7381
+ body: JSON.stringify(command)
7357
7382
  });
7383
+ if (!response.ok) {
7384
+ const text = await response.text().catch(() => "");
7385
+ console.error(`[store][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
7386
+ }
7358
7387
  },
7359
7388
  del: async (key) => {
7360
- await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
7389
+ const response = await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
7361
7390
  method: "POST",
7362
7391
  headers: this.headers()
7363
7392
  });
7393
+ if (!response.ok) {
7394
+ const text = await response.text().catch(() => "");
7395
+ console.error(`[store][upstash] DEL failed (${response.status}): ${text.slice(0, 200)}`);
7396
+ }
7364
7397
  }
7365
7398
  };
7366
7399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "redis": "^5.10.0",
32
32
  "yaml": "^2.4.0",
33
33
  "zod": "^3.22.0",
34
- "@poncho-ai/sdk": "1.5.0"
34
+ "@poncho-ai/sdk": "1.6.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/mustache": "^4.2.6",
package/src/harness.ts CHANGED
@@ -562,7 +562,7 @@ export class AgentHarness {
562
562
  private insideTelemetryCapture = false;
563
563
  private _browserSession?: unknown;
564
564
  private _browserMod?: {
565
- createBrowserTools: (getSession: () => unknown, getConversationId: () => string) => ToolDefinition[];
565
+ createBrowserTools: (getSession: () => unknown) => ToolDefinition[];
566
566
  BrowserSession: new (sessionId: string, config: Record<string, unknown>) => unknown;
567
567
  };
568
568
 
@@ -622,11 +622,7 @@ export class AgentHarness {
622
622
  setSubagentManager(manager: SubagentManager): void {
623
623
  this.subagentManager = manager;
624
624
  this.dispatcher.registerMany(
625
- createSubagentTools(
626
- manager,
627
- () => this._currentRunConversationId,
628
- () => this._currentRunOwnerId ?? "anonymous",
629
- ),
625
+ createSubagentTools(manager),
630
626
  );
631
627
  }
632
628
 
@@ -1165,7 +1161,7 @@ export class AgentHarness {
1165
1161
  private async initBrowserTools(config: PonchoConfig): Promise<void> {
1166
1162
  const spec = ["@poncho-ai", "browser"].join("/");
1167
1163
  let browserMod: {
1168
- createBrowserTools: (getSession: () => unknown, getConversationId: () => string) => ToolDefinition[];
1164
+ createBrowserTools: (getSession: () => unknown) => ToolDefinition[];
1169
1165
  BrowserSession: new (sessionId: string, cfg?: Record<string, unknown>) => unknown;
1170
1166
  };
1171
1167
  try {
@@ -1209,7 +1205,6 @@ export class AgentHarness {
1209
1205
 
1210
1206
  const tools = browserMod.createBrowserTools(
1211
1207
  () => session,
1212
- () => this._currentRunConversationId ?? "__default__",
1213
1208
  );
1214
1209
  for (const tool of tools) {
1215
1210
  if (this.isToolEnabled(tool.name)) {
@@ -1218,10 +1213,6 @@ export class AgentHarness {
1218
1213
  }
1219
1214
  }
1220
1215
 
1221
- /** Conversation ID of the currently executing run (set during run, cleared after). */
1222
- private _currentRunConversationId?: string;
1223
- /** Owner ID of the currently executing run (used by subagent tools). */
1224
- private _currentRunOwnerId?: string;
1225
1216
 
1226
1217
  get browserSession(): unknown {
1227
1218
  return this._browserSession;
@@ -1369,13 +1360,6 @@ export class AgentHarness {
1369
1360
  await this.refreshAgentIfChanged();
1370
1361
  await this.refreshSkillsIfChanged();
1371
1362
 
1372
- // Track which conversation/owner this run belongs to so browser & subagent tools resolve correctly
1373
- this._currentRunConversationId = input.conversationId;
1374
- const ownerParam = input.parameters?.__ownerId;
1375
- if (typeof ownerParam === "string") {
1376
- this._currentRunOwnerId = ownerParam;
1377
- }
1378
-
1379
1363
  let agent = this.parsedAgent as ParsedAgent;
1380
1364
  const runId = `run_${randomUUID()}`;
1381
1365
  const start = now();
@@ -1385,9 +1369,9 @@ export class AgentHarness {
1385
1369
  ? 0 // no hard timeout in development unless explicitly configured
1386
1370
  : (configuredTimeout ?? 300) * 1000;
1387
1371
  const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
1388
- const softDeadlineMs = platformMaxDurationSec > 0
1389
- ? platformMaxDurationSec * 800
1390
- : 0;
1372
+ const softDeadlineMs = (input.disableSoftDeadline || platformMaxDurationSec <= 0)
1373
+ ? 0
1374
+ : platformMaxDurationSec * 800;
1391
1375
  const messages: Message[] = [...(input.messages ?? [])];
1392
1376
  const inputMessageCount = messages.length;
1393
1377
  const events: AgentEvent[] = [];
@@ -1541,6 +1525,18 @@ ${boundedMainMemory.trim()}`
1541
1525
  metadata: { timestamp: now(), id: randomUUID() },
1542
1526
  });
1543
1527
  }
1528
+ } else {
1529
+ // Continuation run (no explicit task). Some providers (Anthropic) require
1530
+ // the conversation to end with a user message. Inject a transient signal
1531
+ // that is sent to the LLM but never persisted to the conversation store.
1532
+ const lastMsg = messages[messages.length - 1];
1533
+ if (lastMsg && lastMsg.role !== "user") {
1534
+ messages.push({
1535
+ role: "user",
1536
+ content: "[System: Your previous turn was interrupted by a time limit. Continue from where you left off — do NOT repeat what you already said. Proceed directly with the next action or tool call.]",
1537
+ metadata: { timestamp: now(), id: randomUUID() },
1538
+ });
1539
+ }
1544
1540
  }
1545
1541
 
1546
1542
  let responseText = "";
@@ -1577,6 +1573,7 @@ ${boundedMainMemory.trim()}`
1577
1573
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
1578
1574
  duration: now() - start,
1579
1575
  continuation: true,
1576
+ continuationMessages: [...messages],
1580
1577
  maxSteps,
1581
1578
  };
1582
1579
  yield pushEvent({ type: "run:completed", runId, result });
@@ -1727,6 +1724,9 @@ ${boundedMainMemory.trim()}`
1727
1724
  } catch {
1728
1725
  // Not JSON, treat as regular assistant text.
1729
1726
  }
1727
+ if (!assistantText || assistantText.trim().length === 0) {
1728
+ return [];
1729
+ }
1730
1730
  return [{ role: "assistant" as const, content: assistantText }];
1731
1731
  }
1732
1732
 
@@ -2403,6 +2403,7 @@ ${boundedMainMemory.trim()}`
2403
2403
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
2404
2404
  duration: now() - start,
2405
2405
  continuation: true,
2406
+ continuationMessages: [...messages],
2406
2407
  maxSteps,
2407
2408
  };
2408
2409
  yield pushEvent({ type: "run:completed", runId, result });
package/src/kv-store.ts CHANGED
@@ -38,17 +38,27 @@ class UpstashKVStore implements RawKVStore {
38
38
  }
39
39
 
40
40
  async set(key: string, value: string): Promise<void> {
41
- await fetch(
42
- `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
43
- { method: "POST", headers: this.headers() },
44
- );
41
+ const response = await fetch(this.baseUrl, {
42
+ method: "POST",
43
+ headers: this.headers(),
44
+ body: JSON.stringify(["SET", key, value]),
45
+ });
46
+ if (!response.ok) {
47
+ const text = await response.text().catch(() => "");
48
+ console.error(`[kv][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
49
+ }
45
50
  }
46
51
 
47
52
  async setWithTtl(key: string, value: string, ttl: number): Promise<void> {
48
- await fetch(
49
- `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(value)}`,
50
- { method: "POST", headers: this.headers() },
51
- );
53
+ const response = await fetch(this.baseUrl, {
54
+ method: "POST",
55
+ headers: this.headers(),
56
+ body: JSON.stringify(["SETEX", key, Math.max(1, ttl), value]),
57
+ });
58
+ if (!response.ok) {
59
+ const text = await response.text().catch(() => "");
60
+ console.error(`[kv][upstash] SETEX failed (${response.status}): ${text.slice(0, 200)}`);
61
+ }
52
62
  }
53
63
  }
54
64
 
package/src/state.ts CHANGED
@@ -21,6 +21,15 @@ export interface StateStore {
21
21
  delete(runId: string): Promise<void>;
22
22
  }
23
23
 
24
+ export interface PendingSubagentResult {
25
+ subagentId: string;
26
+ task: string;
27
+ status: "completed" | "error" | "stopped";
28
+ result?: import("@poncho-ai/sdk").RunResult;
29
+ error?: import("@poncho-ai/sdk").AgentFailure;
30
+ timestamp: number;
31
+ }
32
+
24
33
  export interface Conversation {
25
34
  conversationId: string;
26
35
  title: string;
@@ -55,6 +64,13 @@ export interface Conversation {
55
64
  channelId: string;
56
65
  platformThreadId: string;
57
66
  };
67
+ pendingSubagentResults?: PendingSubagentResult[];
68
+ subagentCallbackCount?: number;
69
+ runningCallbackSince?: number;
70
+ lastActivityAt?: number;
71
+ /** Harness-internal message chain preserved across continuation runs.
72
+ * Cleared when a run completes without continuation. */
73
+ _continuationMessages?: Message[];
58
74
  createdAt: number;
59
75
  updatedAt: number;
60
76
  }
@@ -67,6 +83,7 @@ export interface ConversationStore {
67
83
  update(conversation: Conversation): Promise<void>;
68
84
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
69
85
  delete(conversationId: string): Promise<boolean>;
86
+ appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
70
87
  }
71
88
 
72
89
  export type StateProviderName =
@@ -300,6 +317,14 @@ export class InMemoryConversationStore implements ConversationStore {
300
317
  async delete(conversationId: string): Promise<boolean> {
301
318
  return this.conversations.delete(conversationId);
302
319
  }
320
+
321
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
322
+ const conversation = this.conversations.get(conversationId);
323
+ if (!conversation) return;
324
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
325
+ conversation.pendingSubagentResults.push(result);
326
+ conversation.updatedAt = Date.now();
327
+ }
303
328
  }
304
329
 
305
330
  export type ConversationSummary = {
@@ -572,6 +597,16 @@ class FileConversationStore implements ConversationStore {
572
597
  }
573
598
  return removed;
574
599
  }
600
+
601
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
602
+ await this.ensureLoaded();
603
+ const conversation = await this.get(conversationId);
604
+ if (!conversation) return;
605
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
606
+ conversation.pendingSubagentResults.push(result);
607
+ conversation.updatedAt = Date.now();
608
+ await this.update(conversation);
609
+ }
575
610
  }
576
611
 
577
612
  type LocalStateFile = {
@@ -689,6 +724,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
689
724
  protected readonly ttl?: number;
690
725
  private readonly agentIdPromise: Promise<string>;
691
726
  private readonly ownerLocks = new Map<string, Promise<void>>();
727
+ private readonly appendLocks = new Map<string, Promise<void>>();
692
728
  protected readonly memoryFallback: InMemoryConversationStore;
693
729
 
694
730
  constructor(ttl: number | undefined, workingDir: string, agentId?: string) {
@@ -712,6 +748,19 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
712
748
  }
713
749
  }
714
750
 
751
+ private async withAppendLock(conversationId: string, task: () => Promise<void>): Promise<void> {
752
+ const prev = this.appendLocks.get(conversationId) ?? Promise.resolve();
753
+ const next = prev.then(task, task);
754
+ this.appendLocks.set(conversationId, next);
755
+ try {
756
+ await next;
757
+ } finally {
758
+ if (this.appendLocks.get(conversationId) === next) {
759
+ this.appendLocks.delete(conversationId);
760
+ }
761
+ }
762
+ }
763
+
715
764
  private async namespace(): Promise<string> {
716
765
  const agentId = await this.agentIdPromise;
717
766
  return `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
@@ -945,6 +994,17 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
945
994
  });
946
995
  return true;
947
996
  }
997
+
998
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
999
+ await this.withAppendLock(conversationId, async () => {
1000
+ const conversation = await this.get(conversationId);
1001
+ if (!conversation) return;
1002
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
1003
+ conversation.pendingSubagentResults.push(result);
1004
+ conversation.updatedAt = Date.now();
1005
+ await this.update(conversation);
1006
+ });
1007
+ }
948
1008
  }
949
1009
 
950
1010
  class UpstashConversationStore extends KeyValueConversationStoreBase {
@@ -991,23 +1051,28 @@ class UpstashConversationStore extends KeyValueConversationStoreBase {
991
1051
  return (payload.result ?? []).map((v) => v ?? undefined);
992
1052
  },
993
1053
  set: async (key: string, value: string, ttl?: number) => {
994
- const endpoint =
995
- typeof ttl === "number"
996
- ? `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(
997
- 1,
998
- ttl,
999
- )}/${encodeURIComponent(value)}`
1000
- : `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`;
1001
- await fetch(endpoint, {
1054
+ const command = typeof ttl === "number"
1055
+ ? ["SETEX", key, Math.max(1, ttl), value]
1056
+ : ["SET", key, value];
1057
+ const response = await fetch(this.baseUrl, {
1002
1058
  method: "POST",
1003
1059
  headers: this.headers(),
1060
+ body: JSON.stringify(command),
1004
1061
  });
1062
+ if (!response.ok) {
1063
+ const text = await response.text().catch(() => "");
1064
+ console.error(`[store][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
1065
+ }
1005
1066
  },
1006
1067
  del: async (key: string) => {
1007
- await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
1068
+ const response = await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
1008
1069
  method: "POST",
1009
1070
  headers: this.headers(),
1010
1071
  });
1072
+ if (!response.ok) {
1073
+ const text = await response.text().catch(() => "");
1074
+ console.error(`[store][upstash] DEL failed (${response.status}): ${text.slice(0, 200)}`);
1075
+ }
1011
1076
  },
1012
1077
  };
1013
1078
  }
@@ -15,14 +15,18 @@ export interface SubagentSummary {
15
15
  messageCount: number;
16
16
  }
17
17
 
18
+ export interface SubagentSpawnResult {
19
+ subagentId: string;
20
+ }
21
+
18
22
  export interface SubagentManager {
19
23
  spawn(opts: {
20
24
  task: string;
21
25
  parentConversationId: string;
22
26
  ownerId: string;
23
- }): Promise<SubagentResult>;
27
+ }): Promise<SubagentSpawnResult>;
24
28
 
25
- sendMessage(subagentId: string, message: string): Promise<SubagentResult>;
29
+ sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
26
30
 
27
31
  stop(subagentId: string): Promise<void>;
28
32
 
@@ -1,48 +1,18 @@
1
- import { defineTool, type Message, type ToolDefinition, getTextContent } from "@poncho-ai/sdk";
2
- import type { SubagentManager, SubagentResult } from "./subagent-manager.js";
3
-
4
- const LAST_MESSAGES_TO_RETURN = 10;
5
-
6
- const summarizeResult = (r: SubagentResult): Record<string, unknown> => {
7
- const summary: Record<string, unknown> = {
8
- subagentId: r.subagentId,
9
- status: r.status,
10
- };
11
- if (r.result) {
12
- summary.result = {
13
- status: r.result.status,
14
- response: r.result.response,
15
- steps: r.result.steps,
16
- duration: r.result.duration,
17
- };
18
- }
19
- if (r.error) {
20
- summary.error = r.error;
21
- }
22
- if (r.latestMessages && r.latestMessages.length > 0) {
23
- summary.latestMessages = r.latestMessages
24
- .slice(-LAST_MESSAGES_TO_RETURN)
25
- .map((m: Message) => ({
26
- role: m.role,
27
- content: getTextContent(m).slice(0, 2000),
28
- }));
29
- }
30
- return summary;
31
- };
1
+ import { defineTool, type ToolContext, type ToolDefinition } from "@poncho-ai/sdk";
2
+ import type { SubagentManager } from "./subagent-manager.js";
32
3
 
33
4
  export const createSubagentTools = (
34
5
  manager: SubagentManager,
35
- getConversationId: () => string | undefined,
36
- getOwnerId: () => string,
37
6
  ): ToolDefinition[] => [
38
7
  defineTool({
39
8
  name: "spawn_subagent",
40
9
  description:
41
- "Spawn a subagent to work on a task and wait for it to finish. The subagent is a full copy of " +
42
- "yourself running in its own conversation context with access to the same tools (except memory writes). " +
43
- "This call blocks until the subagent completes and returns its result.\n\n" +
10
+ "Spawn a subagent to work on a task in the background. Returns immediately with a subagent ID. " +
11
+ "The subagent runs independently and its result will be delivered to you as a message in the " +
12
+ "conversation when it completes.\n\n" +
44
13
  "Guidelines:\n" +
45
- "- Use subagents to parallelize work: call spawn_subagent multiple times in one response for independent sub-tasks -- they run concurrently.\n" +
14
+ "- Spawn all needed subagents in a SINGLE response (they run concurrently), then end your turn with a brief message to the user.\n" +
15
+ "- Do NOT spawn more subagents in follow-up steps. Wait for results to be delivered before deciding if more work is needed.\n" +
46
16
  "- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n" +
47
17
  "- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
48
18
  inputSchema: {
@@ -58,29 +28,32 @@ export const createSubagentTools = (
58
28
  required: ["task"],
59
29
  additionalProperties: false,
60
30
  },
61
- handler: async (input) => {
31
+ handler: async (input: Record<string, unknown>, context: ToolContext) => {
62
32
  const task = typeof input.task === "string" ? input.task : "";
63
33
  if (!task.trim()) {
64
34
  return { error: "task is required" };
65
35
  }
66
- const conversationId = getConversationId();
36
+ const conversationId = context.conversationId;
67
37
  if (!conversationId) {
68
38
  return { error: "no active conversation to spawn subagent from" };
69
39
  }
70
- const result = await manager.spawn({
40
+ const ownerId = typeof context.parameters.__ownerId === "string"
41
+ ? context.parameters.__ownerId
42
+ : "anonymous";
43
+ const { subagentId } = await manager.spawn({
71
44
  task: task.trim(),
72
45
  parentConversationId: conversationId,
73
- ownerId: getOwnerId(),
46
+ ownerId,
74
47
  });
75
- return summarizeResult(result);
48
+ return { subagentId, status: "running" };
76
49
  },
77
50
  }),
78
51
 
79
52
  defineTool({
80
53
  name: "message_subagent",
81
54
  description:
82
- "Send a follow-up message to a completed or stopped subagent and wait for it to finish. " +
83
- "This restarts the subagent with the new message and blocks until it completes. " +
55
+ "Send a follow-up message to a completed or stopped subagent. The subagent restarts in the " +
56
+ "background and its result will be delivered to you as a message when it completes. " +
84
57
  "Only works when the subagent is not currently running.",
85
58
  inputSchema: {
86
59
  type: "object",
@@ -103,8 +76,8 @@ export const createSubagentTools = (
103
76
  if (!subagentId || !message.trim()) {
104
77
  return { error: "subagent_id and message are required" };
105
78
  }
106
- const result = await manager.sendMessage(subagentId, message.trim());
107
- return summarizeResult(result);
79
+ const { subagentId: id } = await manager.sendMessage(subagentId, message.trim());
80
+ return { subagentId: id, status: "running" };
108
81
  },
109
82
  }),
110
83
 
@@ -145,8 +118,8 @@ export const createSubagentTools = (
145
118
  properties: {},
146
119
  additionalProperties: false,
147
120
  },
148
- handler: async () => {
149
- const conversationId = getConversationId();
121
+ handler: async (_input: Record<string, unknown>, context: ToolContext) => {
122
+ const conversationId = context.conversationId;
150
123
  if (!conversationId) {
151
124
  return { error: "no active conversation" };
152
125
  }