@poncho-ai/harness 0.26.0 → 0.28.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/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 {
@@ -4246,37 +4258,148 @@ var extractRunnableFunction = (value) => {
4246
4258
  return void 0;
4247
4259
  };
4248
4260
 
4249
- // 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
- }));
4261
+ // src/search-tools.ts
4262
+ import { load as cheerioLoad } from "cheerio";
4263
+ import { defineTool as defineTool5 } from "@poncho-ai/sdk";
4264
+ var SEARCH_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
4265
+ var FETCH_TIMEOUT_MS = 15e3;
4266
+ async function braveSearch(query, maxResults) {
4267
+ const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
4268
+ const res = await fetch(url, {
4269
+ headers: {
4270
+ "User-Agent": SEARCH_UA,
4271
+ Accept: "text/html,application/xhtml+xml",
4272
+ "Accept-Language": "en-US,en;q=0.9"
4273
+ },
4274
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4275
+ });
4276
+ if (!res.ok) {
4277
+ throw new Error(`Search request failed (${res.status} ${res.statusText})`);
4273
4278
  }
4274
- return summary;
4275
- };
4276
- var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4279
+ const html = await res.text();
4280
+ return parseBraveResults(html, maxResults);
4281
+ }
4282
+ function parseBraveResults(html, max) {
4283
+ const $ = cheerioLoad(html);
4284
+ const results = [];
4285
+ $('div.snippet[data-type="web"]').each((_i, el) => {
4286
+ if (results.length >= max) return false;
4287
+ const $el = $(el);
4288
+ const anchor = $el.find(".result-content a").first();
4289
+ const href = anchor.attr("href") ?? "";
4290
+ if (!href.startsWith("http")) return;
4291
+ const title = $el.find(".title").first().text().trim();
4292
+ const snippet = $el.find(".generic-snippet .content").first().text().trim();
4293
+ if (title) {
4294
+ results.push({ title, url: href, snippet });
4295
+ }
4296
+ });
4297
+ return results;
4298
+ }
4299
+ var DEFAULT_MAX_LENGTH = 16e3;
4300
+ function extractReadableText($, maxLength) {
4301
+ const title = $("title").first().text().trim();
4302
+ $("script, style, noscript, nav, footer, header, aside, [role='navigation'], [role='banner'], [role='contentinfo']").remove();
4303
+ $("svg, iframe, form, button, input, select, textarea").remove();
4304
+ let root = $("article").first();
4305
+ if (!root.length) root = $("main").first();
4306
+ if (!root.length) root = $("[role='main']").first();
4307
+ if (!root.length) root = $("body").first();
4308
+ const text = root.text().replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
4309
+ const content = text.length > maxLength ? text.slice(0, maxLength) + "\n\u2026(truncated)" : text;
4310
+ return { title, content };
4311
+ }
4312
+ var createSearchTools = () => [
4277
4313
  defineTool5({
4314
+ name: "web_search",
4315
+ description: "Search the web and return a list of results (title, URL, snippet). Use this instead of opening a browser when you need to find information online.",
4316
+ inputSchema: {
4317
+ type: "object",
4318
+ properties: {
4319
+ query: {
4320
+ type: "string",
4321
+ description: "The search query"
4322
+ },
4323
+ max_results: {
4324
+ type: "number",
4325
+ description: "Maximum number of results to return (1-10, default 5)"
4326
+ }
4327
+ },
4328
+ required: ["query"],
4329
+ additionalProperties: false
4330
+ },
4331
+ handler: async (input) => {
4332
+ const query = typeof input.query === "string" ? input.query.trim() : "";
4333
+ if (!query) {
4334
+ return { error: "A non-empty query string is required." };
4335
+ }
4336
+ const max = Math.min(Math.max(Number(input.max_results) || 5, 1), 10);
4337
+ try {
4338
+ const results = await braveSearch(query, max);
4339
+ if (results.length === 0) {
4340
+ return { query, results: [], note: "No results found. Try rephrasing your query." };
4341
+ }
4342
+ return { query, results };
4343
+ } catch (err) {
4344
+ const msg = err instanceof Error ? err.message : String(err);
4345
+ return {
4346
+ error: `Search failed: ${msg}`,
4347
+ hint: "The search provider may be rate-limiting requests. Try again shortly, or use browser tools as a fallback."
4348
+ };
4349
+ }
4350
+ }
4351
+ }),
4352
+ defineTool5({
4353
+ name: "web_fetch",
4354
+ description: "Fetch a web page and return its text content (HTML tags stripped). Useful for reading articles, documentation, or any web page without opening a browser.",
4355
+ inputSchema: {
4356
+ type: "object",
4357
+ properties: {
4358
+ url: {
4359
+ type: "string",
4360
+ description: "The URL to fetch"
4361
+ },
4362
+ max_length: {
4363
+ type: "number",
4364
+ description: `Maximum character length of returned content (default ${DEFAULT_MAX_LENGTH})`
4365
+ }
4366
+ },
4367
+ required: ["url"],
4368
+ additionalProperties: false
4369
+ },
4370
+ handler: async (input) => {
4371
+ const url = typeof input.url === "string" ? input.url.trim() : "";
4372
+ if (!url) {
4373
+ return { error: 'A "url" string is required.' };
4374
+ }
4375
+ const maxLength = Math.max(Number(input.max_length) || DEFAULT_MAX_LENGTH, 1e3);
4376
+ try {
4377
+ const res = await fetch(url, {
4378
+ headers: { "User-Agent": SEARCH_UA, Accept: "text/html,application/xhtml+xml" },
4379
+ redirect: "follow",
4380
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4381
+ });
4382
+ if (!res.ok) {
4383
+ return { url, status: res.status, error: res.statusText };
4384
+ }
4385
+ const html = await res.text();
4386
+ const $ = cheerioLoad(html);
4387
+ const { title, content } = extractReadableText($, maxLength);
4388
+ return { url, status: res.status, title, content };
4389
+ } catch (err) {
4390
+ const msg = err instanceof Error ? err.message : String(err);
4391
+ return { url, error: `Fetch failed: ${msg}` };
4392
+ }
4393
+ }
4394
+ })
4395
+ ];
4396
+
4397
+ // src/subagent-tools.ts
4398
+ import { defineTool as defineTool6 } from "@poncho-ai/sdk";
4399
+ var createSubagentTools = (manager) => [
4400
+ defineTool6({
4278
4401
  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.",
4402
+ 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
4403
  inputSchema: {
4281
4404
  type: "object",
4282
4405
  properties: {
@@ -4288,26 +4411,27 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4288
4411
  required: ["task"],
4289
4412
  additionalProperties: false
4290
4413
  },
4291
- handler: async (input) => {
4414
+ handler: async (input, context) => {
4292
4415
  const task = typeof input.task === "string" ? input.task : "";
4293
4416
  if (!task.trim()) {
4294
4417
  return { error: "task is required" };
4295
4418
  }
4296
- const conversationId = getConversationId();
4419
+ const conversationId = context.conversationId;
4297
4420
  if (!conversationId) {
4298
4421
  return { error: "no active conversation to spawn subagent from" };
4299
4422
  }
4300
- const result = await manager.spawn({
4423
+ const ownerId = typeof context.parameters.__ownerId === "string" ? context.parameters.__ownerId : "anonymous";
4424
+ const { subagentId } = await manager.spawn({
4301
4425
  task: task.trim(),
4302
4426
  parentConversationId: conversationId,
4303
- ownerId: getOwnerId()
4427
+ ownerId
4304
4428
  });
4305
- return summarizeResult(result);
4429
+ return { subagentId, status: "running" };
4306
4430
  }
4307
4431
  }),
4308
- defineTool5({
4432
+ defineTool6({
4309
4433
  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.",
4434
+ 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
4435
  inputSchema: {
4312
4436
  type: "object",
4313
4437
  properties: {
@@ -4329,11 +4453,11 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4329
4453
  if (!subagentId || !message.trim()) {
4330
4454
  return { error: "subagent_id and message are required" };
4331
4455
  }
4332
- const result = await manager.sendMessage(subagentId, message.trim());
4333
- return summarizeResult(result);
4456
+ const { subagentId: id } = await manager.sendMessage(subagentId, message.trim());
4457
+ return { subagentId: id, status: "running" };
4334
4458
  }
4335
4459
  }),
4336
- defineTool5({
4460
+ defineTool6({
4337
4461
  name: "stop_subagent",
4338
4462
  description: "Stop a running subagent. The subagent's conversation is preserved but it will stop processing. Use this to cancel work that is no longer needed.",
4339
4463
  inputSchema: {
@@ -4356,7 +4480,7 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4356
4480
  return { message: `Subagent "${subagentId}" has been stopped.` };
4357
4481
  }
4358
4482
  }),
4359
- defineTool5({
4483
+ defineTool6({
4360
4484
  name: "list_subagents",
4361
4485
  description: "List all subagents that have been spawned in this conversation. Returns each subagent's ID, original task, current status, and message count. Use this to look up subagent IDs before calling message_subagent or stop_subagent.",
4362
4486
  inputSchema: {
@@ -4364,8 +4488,8 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4364
4488
  properties: {},
4365
4489
  additionalProperties: false
4366
4490
  },
4367
- handler: async () => {
4368
- const conversationId = getConversationId();
4491
+ handler: async (_input, context) => {
4492
+ const conversationId = context.conversationId;
4369
4493
  if (!conversationId) {
4370
4494
  return { error: "no active conversation" };
4371
4495
  }
@@ -4961,11 +5085,7 @@ var AgentHarness = class _AgentHarness {
4961
5085
  setSubagentManager(manager) {
4962
5086
  this.subagentManager = manager;
4963
5087
  this.dispatcher.registerMany(
4964
- createSubagentTools(
4965
- manager,
4966
- () => this._currentRunConversationId,
4967
- () => this._currentRunOwnerId ?? "anonymous"
4968
- )
5088
+ createSubagentTools(manager)
4969
5089
  );
4970
5090
  }
4971
5091
  registerConfiguredBuiltInTools(config) {
@@ -4986,6 +5106,11 @@ var AgentHarness = class _AgentHarness {
4986
5106
  if (this.isToolEnabled("delete_directory")) {
4987
5107
  this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
4988
5108
  }
5109
+ for (const tool of createSearchTools()) {
5110
+ if (this.isToolEnabled(tool.name)) {
5111
+ this.registerIfMissing(tool);
5112
+ }
5113
+ }
4989
5114
  if (this.environment === "development" && this.isToolEnabled("poncho_docs")) {
4990
5115
  this.registerIfMissing(ponchoDocsTool);
4991
5116
  }
@@ -5472,7 +5597,10 @@ var AgentHarness = class _AgentHarness {
5472
5597
  this._browserSession = session;
5473
5598
  const tools = browserMod.createBrowserTools(
5474
5599
  () => session,
5475
- () => this._currentRunConversationId ?? "__default__"
5600
+ // Backward compat: older @poncho-ai/browser versions expect a second
5601
+ // getConversationId callback. Current versions read from ToolContext
5602
+ // and ignore extra args.
5603
+ () => "__default__"
5476
5604
  );
5477
5605
  for (const tool of tools) {
5478
5606
  if (this.isToolEnabled(tool.name)) {
@@ -5480,10 +5608,6 @@ var AgentHarness = class _AgentHarness {
5480
5608
  }
5481
5609
  }
5482
5610
  }
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
5611
  get browserSession() {
5488
5612
  return this._browserSession;
5489
5613
  }
@@ -5598,11 +5722,6 @@ var AgentHarness = class _AgentHarness {
5598
5722
  const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
5599
5723
  await this.refreshAgentIfChanged();
5600
5724
  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
5725
  let agent = this.parsedAgent;
5607
5726
  const runId = `run_${randomUUID3()}`;
5608
5727
  const start = now();
@@ -5610,7 +5729,7 @@ var AgentHarness = class _AgentHarness {
5610
5729
  const configuredTimeout = agent.frontmatter.limits?.timeout;
5611
5730
  const timeoutMs = this.environment === "development" && configuredTimeout == null ? 0 : (configuredTimeout ?? 300) * 1e3;
5612
5731
  const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
5613
- const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
5732
+ const softDeadlineMs = input.disableSoftDeadline || platformMaxDurationSec <= 0 ? 0 : platformMaxDurationSec * 800;
5614
5733
  const messages = [...input.messages ?? []];
5615
5734
  const inputMessageCount = messages.length;
5616
5735
  const events = [];
@@ -5744,6 +5863,15 @@ ${this.skillFingerprint}`;
5744
5863
  metadata: { timestamp: now(), id: randomUUID3() }
5745
5864
  });
5746
5865
  }
5866
+ } else {
5867
+ const lastMsg = messages[messages.length - 1];
5868
+ if (lastMsg && lastMsg.role !== "user") {
5869
+ messages.push({
5870
+ role: "user",
5871
+ 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.]",
5872
+ metadata: { timestamp: now(), id: randomUUID3() }
5873
+ });
5874
+ }
5747
5875
  }
5748
5876
  let responseText = "";
5749
5877
  let totalInputTokens = 0;
@@ -5778,6 +5906,7 @@ ${this.skillFingerprint}`;
5778
5906
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
5779
5907
  duration: now() - start,
5780
5908
  continuation: true,
5909
+ continuationMessages: [...messages],
5781
5910
  maxSteps
5782
5911
  };
5783
5912
  yield pushEvent({ type: "run:completed", runId, result: result2 });
@@ -5811,7 +5940,7 @@ ${this.skillFingerprint}`;
5811
5940
  if (rich && rich.length > 0) {
5812
5941
  return [{ role: "tool", content: rich }];
5813
5942
  }
5814
- const textContent = typeof msg.content === "string" ? msg.content : getTextContent3(msg);
5943
+ const textContent = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
5815
5944
  try {
5816
5945
  const parsed = JSON.parse(textContent);
5817
5946
  if (!Array.isArray(parsed)) {
@@ -5861,7 +5990,7 @@ ${this.skillFingerprint}`;
5861
5990
  }
5862
5991
  }
5863
5992
  if (msg.role === "assistant") {
5864
- const assistantText = typeof msg.content === "string" ? msg.content : getTextContent3(msg);
5993
+ const assistantText = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
5865
5994
  try {
5866
5995
  const parsed = JSON.parse(assistantText);
5867
5996
  if (typeof parsed === "object" && parsed !== null) {
@@ -5895,12 +6024,15 @@ ${this.skillFingerprint}`;
5895
6024
  }
5896
6025
  } catch {
5897
6026
  }
6027
+ if (!assistantText || assistantText.trim().length === 0) {
6028
+ return [];
6029
+ }
5898
6030
  return [{ role: "assistant", content: assistantText }];
5899
6031
  }
5900
6032
  if (msg.role === "system") {
5901
6033
  return [{
5902
6034
  role: "system",
5903
- content: typeof msg.content === "string" ? msg.content : getTextContent3(msg)
6035
+ content: typeof msg.content === "string" ? msg.content : getTextContent2(msg)
5904
6036
  }];
5905
6037
  }
5906
6038
  if (msg.role === "user") {
@@ -6454,6 +6586,7 @@ ${this.skillFingerprint}`;
6454
6586
  tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6455
6587
  duration: now() - start,
6456
6588
  continuation: true,
6589
+ continuationMessages: [...messages],
6457
6590
  maxSteps
6458
6591
  };
6459
6592
  yield pushEvent({ type: "run:completed", runId, result });
@@ -6775,6 +6908,13 @@ var InMemoryConversationStore = class {
6775
6908
  async delete(conversationId) {
6776
6909
  return this.conversations.delete(conversationId);
6777
6910
  }
6911
+ async appendSubagentResult(conversationId, result) {
6912
+ const conversation = this.conversations.get(conversationId);
6913
+ if (!conversation) return;
6914
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
6915
+ conversation.pendingSubagentResults.push(result);
6916
+ conversation.updatedAt = Date.now();
6917
+ }
6778
6918
  };
6779
6919
  var FileConversationStore = class {
6780
6920
  workingDir;
@@ -6988,6 +7128,15 @@ var FileConversationStore = class {
6988
7128
  }
6989
7129
  return removed;
6990
7130
  }
7131
+ async appendSubagentResult(conversationId, result) {
7132
+ await this.ensureLoaded();
7133
+ const conversation = await this.get(conversationId);
7134
+ if (!conversation) return;
7135
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
7136
+ conversation.pendingSubagentResults.push(result);
7137
+ conversation.updatedAt = Date.now();
7138
+ await this.update(conversation);
7139
+ }
6991
7140
  };
6992
7141
  var FileStateStore = class {
6993
7142
  workingDir;
@@ -7067,6 +7216,7 @@ var KeyValueConversationStoreBase = class {
7067
7216
  ttl;
7068
7217
  agentIdPromise;
7069
7218
  ownerLocks = /* @__PURE__ */ new Map();
7219
+ appendLocks = /* @__PURE__ */ new Map();
7070
7220
  memoryFallback;
7071
7221
  constructor(ttl, workingDir, agentId) {
7072
7222
  this.ttl = ttl;
@@ -7085,6 +7235,18 @@ var KeyValueConversationStoreBase = class {
7085
7235
  }
7086
7236
  }
7087
7237
  }
7238
+ async withAppendLock(conversationId, task) {
7239
+ const prev = this.appendLocks.get(conversationId) ?? Promise.resolve();
7240
+ const next = prev.then(task, task);
7241
+ this.appendLocks.set(conversationId, next);
7242
+ try {
7243
+ await next;
7244
+ } finally {
7245
+ if (this.appendLocks.get(conversationId) === next) {
7246
+ this.appendLocks.delete(conversationId);
7247
+ }
7248
+ }
7249
+ }
7088
7250
  async namespace() {
7089
7251
  const agentId = await this.agentIdPromise;
7090
7252
  return `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
@@ -7305,6 +7467,16 @@ var KeyValueConversationStoreBase = class {
7305
7467
  });
7306
7468
  return true;
7307
7469
  }
7470
+ async appendSubagentResult(conversationId, result) {
7471
+ await this.withAppendLock(conversationId, async () => {
7472
+ const conversation = await this.get(conversationId);
7473
+ if (!conversation) return;
7474
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
7475
+ conversation.pendingSubagentResults.push(result);
7476
+ conversation.updatedAt = Date.now();
7477
+ await this.update(conversation);
7478
+ });
7479
+ }
7308
7480
  };
7309
7481
  var UpstashConversationStore = class extends KeyValueConversationStoreBase {
7310
7482
  baseUrl;
@@ -7347,20 +7519,26 @@ var UpstashConversationStore = class extends KeyValueConversationStoreBase {
7347
7519
  return (payload.result ?? []).map((v) => v ?? void 0);
7348
7520
  },
7349
7521
  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, {
7522
+ const command = typeof ttl === "number" ? ["SETEX", key, Math.max(1, ttl), value] : ["SET", key, value];
7523
+ const response = await fetch(this.baseUrl, {
7355
7524
  method: "POST",
7356
- headers: this.headers()
7525
+ headers: this.headers(),
7526
+ body: JSON.stringify(command)
7357
7527
  });
7528
+ if (!response.ok) {
7529
+ const text = await response.text().catch(() => "");
7530
+ console.error(`[store][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
7531
+ }
7358
7532
  },
7359
7533
  del: async (key) => {
7360
- await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
7534
+ const response = await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
7361
7535
  method: "POST",
7362
7536
  headers: this.headers()
7363
7537
  });
7538
+ if (!response.ok) {
7539
+ const text = await response.text().catch(() => "");
7540
+ console.error(`[store][upstash] DEL failed (${response.status}): ${text.slice(0, 200)}`);
7541
+ }
7364
7542
  }
7365
7543
  };
7366
7544
  }
@@ -7749,7 +7927,7 @@ var TelemetryEmitter = class {
7749
7927
  };
7750
7928
 
7751
7929
  // src/index.ts
7752
- import { defineTool as defineTool6 } from "@poncho-ai/sdk";
7930
+ import { defineTool as defineTool7 } from "@poncho-ai/sdk";
7753
7931
  export {
7754
7932
  AgentHarness,
7755
7933
  InMemoryConversationStore,
@@ -7774,12 +7952,13 @@ export {
7774
7952
  createMemoryStore,
7775
7953
  createMemoryTools,
7776
7954
  createModelProvider,
7955
+ createSearchTools,
7777
7956
  createSkillTools,
7778
7957
  createStateStore,
7779
7958
  createSubagentTools,
7780
7959
  createUploadStore,
7781
7960
  createWriteTool,
7782
- defineTool6 as defineTool,
7961
+ defineTool7 as defineTool,
7783
7962
  deriveUploadKey,
7784
7963
  ensureAgentIdentity,
7785
7964
  estimateTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,12 +26,13 @@
26
26
  "@latitude-data/telemetry": "^2.0.4",
27
27
  "@opentelemetry/api": "1.9.0",
28
28
  "ai": "^6.0.86",
29
+ "cheerio": "^1.2.0",
29
30
  "jiti": "^2.6.1",
30
31
  "mustache": "^4.2.0",
31
32
  "redis": "^5.10.0",
32
33
  "yaml": "^2.4.0",
33
34
  "zod": "^3.22.0",
34
- "@poncho-ai/sdk": "1.5.0"
35
+ "@poncho-ai/sdk": "1.6.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/mustache": "^4.2.6",
package/src/config.ts CHANGED
@@ -46,6 +46,8 @@ export type BuiltInToolToggles = {
46
46
  todo_add?: boolean;
47
47
  todo_update?: boolean;
48
48
  todo_remove?: boolean;
49
+ web_search?: boolean;
50
+ web_fetch?: boolean;
49
51
  };
50
52
 
51
53
  export interface MessagingChannelConfig {