@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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +22 -0
- package/dist/index.d.ts +24 -8
- package/dist/index.js +115 -82
- package/package.json +2 -2
- package/src/harness.ts +23 -22
- package/src/kv-store.ts +18 -8
- package/src/state.ts +74 -9
- package/src/subagent-manager.ts +6 -2
- package/src/subagent-tools.ts +21 -48
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
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
|
[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[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m278.61 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 148ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 7054ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m29.13 KB[39m
|
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<
|
|
539
|
-
sendMessage(subagentId: string, message: string): Promise<
|
|
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
|
|
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
|
|
621
|
-
|
|
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
|
|
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
|
|
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.
|
|
1306
|
-
| \`message_subagent\` | Send a follow-up message to an existing subagent.
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
|
|
2373
|
-
|
|
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
|
-
|
|
2379
|
-
|
|
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
|
|
4251
|
-
var
|
|
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
|
|
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 =
|
|
4283
|
+
const conversationId = context.conversationId;
|
|
4297
4284
|
if (!conversationId) {
|
|
4298
4285
|
return { error: "no active conversation to spawn subagent from" };
|
|
4299
4286
|
}
|
|
4300
|
-
const
|
|
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
|
|
4291
|
+
ownerId
|
|
4304
4292
|
});
|
|
4305
|
-
return
|
|
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
|
|
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
|
|
4333
|
-
return
|
|
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 =
|
|
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
|
|
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 :
|
|
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 :
|
|
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 :
|
|
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
|
|
7351
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1389
|
-
?
|
|
1390
|
-
:
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
}
|
package/src/subagent-manager.ts
CHANGED
|
@@ -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<
|
|
27
|
+
}): Promise<SubagentSpawnResult>;
|
|
24
28
|
|
|
25
|
-
sendMessage(subagentId: string, message: string): Promise<
|
|
29
|
+
sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
|
|
26
30
|
|
|
27
31
|
stop(subagentId: string): Promise<void>;
|
|
28
32
|
|
package/src/subagent-tools.ts
CHANGED
|
@@ -1,48 +1,18 @@
|
|
|
1
|
-
import { defineTool, type
|
|
2
|
-
import type { SubagentManager
|
|
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
|
|
42
|
-
"
|
|
43
|
-
"
|
|
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
|
-
"-
|
|
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 =
|
|
36
|
+
const conversationId = context.conversationId;
|
|
67
37
|
if (!conversationId) {
|
|
68
38
|
return { error: "no active conversation to spawn subagent from" };
|
|
69
39
|
}
|
|
70
|
-
const
|
|
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
|
|
46
|
+
ownerId,
|
|
74
47
|
});
|
|
75
|
-
return
|
|
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
|
|
83
|
-
"
|
|
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
|
|
107
|
-
return
|
|
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 =
|
|
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
|
}
|