@poncho-ai/harness 0.16.0 → 0.17.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.16.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.17.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 185.71 KB
11
- ESM ⚡️ Build success in 140ms
10
+ ESM dist/index.js 196.31 KB
11
+ ESM ⚡️ Build success in 155ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 7525ms
14
- DTS dist/index.d.ts 22.38 KB
13
+ DTS ⚡️ Build success in 6569ms
14
+ DTS dist/index.d.ts 23.97 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#16](https://github.com/cesr/poncho-ai/pull/16) [`972577d`](https://github.com/cesr/poncho-ai/commit/972577d255ab43c2c56f3c3464042a8a617b7f9e) Thanks [@cesr](https://github.com/cesr)! - Add subagent support: agents can spawn recursive copies of themselves as independent sub-conversations with blocking tool calls, read-only memory, approval tunneling to the parent thread, and nested sidebar display in the web UI. Also adds ConversationStore.listSummaries() for fast sidebar loading without reading full conversation files from disk.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`972577d`](https://github.com/cesr/poncho-ai/commit/972577d255ab43c2c56f3c3464042a8a617b7f9e)]:
12
+ - @poncho-ai/sdk@1.2.0
13
+
14
+ ## 0.16.1
15
+
16
+ ### Patch Changes
17
+
18
+ - [`7475da5`](https://github.com/cesr/poncho-ai/commit/7475da5c0c2399e79064a2622137c0eb2fb16871) Thanks [@cesr](https://github.com/cesr)! - Inject browser usage context into agent system prompt (auth flow, session persistence, tool selection guidance).
19
+
3
20
  ## 0.16.0
4
21
 
5
22
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Message, ToolDefinition, ToolContext, RunInput, AgentEvent, RunResult, JsonSchema } from '@poncho-ai/sdk';
1
+ import * as _poncho_ai_sdk from '@poncho-ai/sdk';
2
+ import { Message, ToolDefinition, RunResult, AgentFailure, ToolContext, RunInput, AgentEvent, JsonSchema } from '@poncho-ai/sdk';
2
3
  export { ToolDefinition, defineTool } from '@poncho-ai/sdk';
3
4
  import { LanguageModel } from 'ai';
4
5
  import { z } from 'zod';
@@ -96,11 +97,21 @@ interface Conversation {
96
97
  }>;
97
98
  ownerId: string;
98
99
  tenantId: string | null;
100
+ contextTokens?: number;
101
+ contextWindow?: number;
102
+ parentConversationId?: string;
103
+ subagentMeta?: {
104
+ task: string;
105
+ status: "running" | "completed" | "error" | "stopped";
106
+ result?: _poncho_ai_sdk.RunResult;
107
+ error?: _poncho_ai_sdk.AgentFailure;
108
+ };
99
109
  createdAt: number;
100
110
  updatedAt: number;
101
111
  }
102
112
  interface ConversationStore {
103
113
  list(ownerId?: string): Promise<Conversation[]>;
114
+ listSummaries(ownerId?: string): Promise<ConversationSummary[]>;
104
115
  get(conversationId: string): Promise<Conversation | undefined>;
105
116
  create(ownerId?: string, title?: string): Promise<Conversation>;
106
117
  update(conversation: Conversation): Promise<void>;
@@ -132,12 +143,23 @@ declare class InMemoryConversationStore implements ConversationStore {
132
143
  private isExpired;
133
144
  private purgeExpired;
134
145
  list(ownerId?: string): Promise<Conversation[]>;
146
+ listSummaries(ownerId?: string): Promise<ConversationSummary[]>;
135
147
  get(conversationId: string): Promise<Conversation | undefined>;
136
148
  create(ownerId?: string, title?: string): Promise<Conversation>;
137
149
  update(conversation: Conversation): Promise<void>;
138
150
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
139
151
  delete(conversationId: string): Promise<boolean>;
140
152
  }
153
+ type ConversationSummary = {
154
+ conversationId: string;
155
+ title: string;
156
+ updatedAt: number;
157
+ createdAt?: number;
158
+ ownerId: string;
159
+ parentConversationId?: string;
160
+ messageCount?: number;
161
+ hasPendingApprovals?: boolean;
162
+ };
141
163
  declare const createStateStore: (config?: StateConfig, options?: {
142
164
  workingDir?: string;
143
165
  agentId?: string;
@@ -400,6 +422,30 @@ interface ProviderConfig {
400
422
  */
401
423
  declare const createModelProvider: (provider?: string, config?: ProviderConfig) => ModelProviderFactory;
402
424
 
425
+ interface SubagentResult {
426
+ subagentId: string;
427
+ status: "completed" | "error" | "stopped";
428
+ latestMessages?: Message[];
429
+ result?: RunResult;
430
+ error?: AgentFailure;
431
+ }
432
+ interface SubagentSummary {
433
+ subagentId: string;
434
+ task: string;
435
+ status: string;
436
+ messageCount: number;
437
+ }
438
+ interface SubagentManager {
439
+ spawn(opts: {
440
+ task: string;
441
+ parentConversationId: string;
442
+ ownerId: string;
443
+ }): Promise<SubagentResult>;
444
+ sendMessage(subagentId: string, message: string): Promise<SubagentResult>;
445
+ stop(subagentId: string): Promise<void>;
446
+ list(parentConversationId: string): Promise<SubagentSummary[]>;
447
+ }
448
+
403
449
  interface ToolCall {
404
450
  id: string;
405
451
  name: string;
@@ -456,6 +502,7 @@ declare class AgentHarness {
456
502
  private _browserMod?;
457
503
  private parsedAgent?;
458
504
  private mcpBridge?;
505
+ private subagentManager?;
459
506
  private resolveToolAccess;
460
507
  private isToolEnabled;
461
508
  private registerIfMissing;
@@ -465,6 +512,8 @@ declare class AgentHarness {
465
512
  * Tools disabled via `tools` config are skipped.
466
513
  */
467
514
  registerTools(tools: ToolDefinition[]): void;
515
+ unregisterTools(names: string[]): void;
516
+ setSubagentManager(manager: SubagentManager): void;
468
517
  private registerConfiguredBuiltInTools;
469
518
  private shouldEnableWriteTool;
470
519
  constructor(options?: HarnessOptions);
@@ -490,6 +539,8 @@ declare class AgentHarness {
490
539
  private initBrowserTools;
491
540
  /** Conversation ID of the currently executing run (set during run, cleared after). */
492
541
  private _currentRunConversationId?;
542
+ /** Owner ID of the currently executing run (used by subagent tools). */
543
+ private _currentRunOwnerId?;
493
544
  get browserSession(): unknown;
494
545
  shutdown(): Promise<void>;
495
546
  listTools(): ToolDefinition[];
@@ -515,16 +566,6 @@ declare class AgentHarness {
515
566
  runToCompletion(input: RunInput): Promise<HarnessRunOutput>;
516
567
  }
517
568
 
518
- /**
519
- * Latitude telemetry integration for Vercel AI SDK
520
- *
521
- * TODO: Implement proper Vercel AI SDK telemetry integration using:
522
- * - LatitudeTelemetry.capture() wrapper around streamText()
523
- * - experimental_telemetry: { isEnabled: true } in streamText() options
524
- *
525
- * This requires @latitude-data/telemetry package which has official
526
- * Vercel AI SDK support.
527
- */
528
569
  interface LatitudeCaptureConfig {
529
570
  apiKeyEnv?: string;
530
571
  projectIdEnv?: string;
@@ -532,8 +573,9 @@ interface LatitudeCaptureConfig {
532
573
  defaultPath?: string;
533
574
  }
534
575
  /**
535
- * Placeholder for Latitude telemetry integration
536
- * This will be properly implemented once Vercel AI SDK migration is complete
576
+ * Reads and validates Latitude telemetry configuration from environment
577
+ * variables. The actual telemetry capture is handled by LatitudeTelemetry
578
+ * from @latitude-data/telemetry in harness.ts (via runWithTelemetry).
537
579
  */
538
580
  declare class LatitudeCapture {
539
581
  private readonly apiKey?;
@@ -636,4 +678,6 @@ declare class TelemetryEmitter {
636
678
  private sendOtlp;
637
679
  }
638
680
 
639
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, 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 TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
681
+ declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
682
+
683
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, 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, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -466,7 +466,7 @@ var createWriteTool = (workingDir) => defineTool({
466
466
 
467
467
  // src/harness.ts
468
468
  import { randomUUID as randomUUID3 } from "crypto";
469
- import { getTextContent } from "@poncho-ai/sdk";
469
+ import { getTextContent as getTextContent2 } from "@poncho-ai/sdk";
470
470
 
471
471
  // src/upload-store.ts
472
472
  import { createHash as createHash2 } from "crypto";
@@ -2447,6 +2447,138 @@ var extractRunnableFunction = (value) => {
2447
2447
  return void 0;
2448
2448
  };
2449
2449
 
2450
+ // src/subagent-tools.ts
2451
+ import { defineTool as defineTool4, getTextContent } from "@poncho-ai/sdk";
2452
+ var LAST_MESSAGES_TO_RETURN = 10;
2453
+ var summarizeResult = (r) => {
2454
+ const summary = {
2455
+ subagentId: r.subagentId,
2456
+ status: r.status
2457
+ };
2458
+ if (r.result) {
2459
+ summary.result = {
2460
+ status: r.result.status,
2461
+ response: r.result.response,
2462
+ steps: r.result.steps,
2463
+ duration: r.result.duration
2464
+ };
2465
+ }
2466
+ if (r.error) {
2467
+ summary.error = r.error;
2468
+ }
2469
+ if (r.latestMessages && r.latestMessages.length > 0) {
2470
+ summary.latestMessages = r.latestMessages.slice(-LAST_MESSAGES_TO_RETURN).map((m) => ({
2471
+ role: m.role,
2472
+ content: getTextContent(m).slice(0, 2e3)
2473
+ }));
2474
+ }
2475
+ return summary;
2476
+ };
2477
+ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
2478
+ defineTool4({
2479
+ name: "spawn_subagent",
2480
+ 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.",
2481
+ inputSchema: {
2482
+ type: "object",
2483
+ properties: {
2484
+ task: {
2485
+ type: "string",
2486
+ description: "Thorough, self-contained instructions for the subagent. Include all relevant context, goals, and constraints -- the subagent starts with zero prior conversation history."
2487
+ }
2488
+ },
2489
+ required: ["task"],
2490
+ additionalProperties: false
2491
+ },
2492
+ handler: async (input) => {
2493
+ const task = typeof input.task === "string" ? input.task : "";
2494
+ if (!task.trim()) {
2495
+ return { error: "task is required" };
2496
+ }
2497
+ const conversationId = getConversationId();
2498
+ if (!conversationId) {
2499
+ return { error: "no active conversation to spawn subagent from" };
2500
+ }
2501
+ const result = await manager.spawn({
2502
+ task: task.trim(),
2503
+ parentConversationId: conversationId,
2504
+ ownerId: getOwnerId()
2505
+ });
2506
+ return summarizeResult(result);
2507
+ }
2508
+ }),
2509
+ defineTool4({
2510
+ name: "message_subagent",
2511
+ 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.",
2512
+ inputSchema: {
2513
+ type: "object",
2514
+ properties: {
2515
+ subagent_id: {
2516
+ type: "string",
2517
+ description: "The subagent ID (from spawn_subagent result or list_subagents)."
2518
+ },
2519
+ message: {
2520
+ type: "string",
2521
+ description: "The follow-up instructions or message to send."
2522
+ }
2523
+ },
2524
+ required: ["subagent_id", "message"],
2525
+ additionalProperties: false
2526
+ },
2527
+ handler: async (input) => {
2528
+ const subagentId = typeof input.subagent_id === "string" ? input.subagent_id : "";
2529
+ const message = typeof input.message === "string" ? input.message : "";
2530
+ if (!subagentId || !message.trim()) {
2531
+ return { error: "subagent_id and message are required" };
2532
+ }
2533
+ const result = await manager.sendMessage(subagentId, message.trim());
2534
+ return summarizeResult(result);
2535
+ }
2536
+ }),
2537
+ defineTool4({
2538
+ name: "stop_subagent",
2539
+ 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.",
2540
+ inputSchema: {
2541
+ type: "object",
2542
+ properties: {
2543
+ subagent_id: {
2544
+ type: "string",
2545
+ description: "The subagent ID (from spawn_subagent result or list_subagents)."
2546
+ }
2547
+ },
2548
+ required: ["subagent_id"],
2549
+ additionalProperties: false
2550
+ },
2551
+ handler: async (input) => {
2552
+ const subagentId = typeof input.subagent_id === "string" ? input.subagent_id : "";
2553
+ if (!subagentId) {
2554
+ return { error: "subagent_id is required" };
2555
+ }
2556
+ await manager.stop(subagentId);
2557
+ return { message: `Subagent "${subagentId}" has been stopped.` };
2558
+ }
2559
+ }),
2560
+ defineTool4({
2561
+ name: "list_subagents",
2562
+ 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.",
2563
+ inputSchema: {
2564
+ type: "object",
2565
+ properties: {},
2566
+ additionalProperties: false
2567
+ },
2568
+ handler: async () => {
2569
+ const conversationId = getConversationId();
2570
+ if (!conversationId) {
2571
+ return { error: "no active conversation" };
2572
+ }
2573
+ const subagents = await manager.list(conversationId);
2574
+ if (subagents.length === 0) {
2575
+ return { message: "No subagents have been spawned in this conversation." };
2576
+ }
2577
+ return { subagents };
2578
+ }
2579
+ })
2580
+ ];
2581
+
2450
2582
  // src/harness.ts
2451
2583
  import { LatitudeTelemetry } from "@latitude-data/telemetry";
2452
2584
  import { diag, DiagLogLevel } from "@opentelemetry/api";
@@ -2928,6 +3060,7 @@ var AgentHarness = class {
2928
3060
  _browserMod;
2929
3061
  parsedAgent;
2930
3062
  mcpBridge;
3063
+ subagentManager;
2931
3064
  resolveToolAccess(toolName) {
2932
3065
  const tools = this.loadedConfig?.tools;
2933
3066
  if (!tools) return true;
@@ -2964,6 +3097,19 @@ var AgentHarness = class {
2964
3097
  this.dispatcher.register(tool);
2965
3098
  }
2966
3099
  }
3100
+ unregisterTools(names) {
3101
+ this.dispatcher.unregisterMany(names);
3102
+ }
3103
+ setSubagentManager(manager) {
3104
+ this.subagentManager = manager;
3105
+ this.dispatcher.registerMany(
3106
+ createSubagentTools(
3107
+ manager,
3108
+ () => this._currentRunConversationId,
3109
+ () => this._currentRunOwnerId ?? "anonymous"
3110
+ )
3111
+ );
3112
+ }
2967
3113
  registerConfiguredBuiltInTools(config) {
2968
3114
  for (const tool of createDefaultTools(this.workingDir)) {
2969
3115
  if (this.isToolEnabled(tool.name)) {
@@ -3418,6 +3564,8 @@ var AgentHarness = class {
3418
3564
  }
3419
3565
  /** Conversation ID of the currently executing run (set during run, cleared after). */
3420
3566
  _currentRunConversationId;
3567
+ /** Owner ID of the currently executing run (used by subagent tools). */
3568
+ _currentRunOwnerId;
3421
3569
  get browserSession() {
3422
3570
  return this._browserSession;
3423
3571
  }
@@ -3515,6 +3663,10 @@ var AgentHarness = class {
3515
3663
  }
3516
3664
  await this.refreshSkillsIfChanged();
3517
3665
  this._currentRunConversationId = input.conversationId;
3666
+ const ownerParam = input.parameters?.__ownerId;
3667
+ if (typeof ownerParam === "string") {
3668
+ this._currentRunOwnerId = ownerParam;
3669
+ }
3518
3670
  const agent = this.parsedAgent;
3519
3671
  const runId = `run_${randomUUID3()}`;
3520
3672
  const start = now();
@@ -3538,9 +3690,29 @@ var AgentHarness = class {
3538
3690
  const developmentContext = this.environment === "development" ? `
3539
3691
 
3540
3692
  ${DEVELOPMENT_MODE_CONTEXT}` : "";
3693
+ const browserContext = this._browserSession ? `
3694
+
3695
+ ## Browser Tools
3696
+
3697
+ The user has a live browser viewport displayed alongside the conversation. They can see everything the browser shows in real time and interact with it directly (click, type, scroll, paste).
3698
+
3699
+ ### Authentication
3700
+ When a website requires authentication or credentials, do NOT ask the user to send them in the chat. Instead, navigate to the login page and let the user enter their credentials directly in the browser viewport. Wait for them to confirm they have logged in before continuing.
3701
+
3702
+ ### Session persistence
3703
+ Browser sessions (cookies, localStorage, login state) are automatically saved and restored across conversations. If the user logged into a website in a previous conversation, that session is likely still active. Try navigating directly to the authenticated page before asking the user to log in again.
3704
+
3705
+ ### Reading page content
3706
+ - Use \`browser_content\` to read the visible text on a page. This is fast and token-efficient.
3707
+ - Use \`browser_snapshot\` to get the accessibility tree with interactive element refs for clicking and typing.
3708
+ - Use \`browser_screenshot\` only when you need to see visual layout or images. Screenshots consume significantly more tokens.
3709
+ - The accessibility tree may be sparse on some pages. If \`browser_snapshot\` returns little or no content, fall back to \`browser_content\` or \`browser_screenshot\`.
3710
+
3711
+ ### Tabs and resources
3712
+ Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.` : "";
3541
3713
  const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
3542
3714
 
3543
- ${this.skillContextWindow}` : `${systemPrompt}${developmentContext}`;
3715
+ ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
3544
3716
  const mainMemory = this.memoryStore ? await this.memoryStore.getMainMemory() : void 0;
3545
3717
  const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
3546
3718
  ...[truncated]` : mainMemory?.content;
@@ -3695,7 +3867,7 @@ ${boundedMainMemory.trim()}` : "";
3695
3867
  if (rich && rich.length > 0) {
3696
3868
  return [{ role: "tool", content: rich }];
3697
3869
  }
3698
- const textContent = typeof msg.content === "string" ? msg.content : getTextContent(msg);
3870
+ const textContent = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
3699
3871
  try {
3700
3872
  const parsed = JSON.parse(textContent);
3701
3873
  if (!Array.isArray(parsed)) {
@@ -3745,7 +3917,7 @@ ${boundedMainMemory.trim()}` : "";
3745
3917
  }
3746
3918
  }
3747
3919
  if (msg.role === "assistant") {
3748
- const assistantText = typeof msg.content === "string" ? msg.content : getTextContent(msg);
3920
+ const assistantText = typeof msg.content === "string" ? msg.content : getTextContent2(msg);
3749
3921
  try {
3750
3922
  const parsed = JSON.parse(assistantText);
3751
3923
  if (typeof parsed === "object" && parsed !== null) {
@@ -3784,7 +3956,7 @@ ${boundedMainMemory.trim()}` : "";
3784
3956
  if (msg.role === "system") {
3785
3957
  return [{
3786
3958
  role: "system",
3787
- content: typeof msg.content === "string" ? msg.content : getTextContent(msg)
3959
+ content: typeof msg.content === "string" ? msg.content : getTextContent2(msg)
3788
3960
  }];
3789
3961
  }
3790
3962
  if (msg.role === "user") {
@@ -4054,7 +4226,8 @@ ${textContent}` };
4054
4226
  step,
4055
4227
  workingDir: this.workingDir,
4056
4228
  parameters: input.parameters ?? {},
4057
- abortSignal: input.abortSignal
4229
+ abortSignal: input.abortSignal,
4230
+ conversationId: input.conversationId
4058
4231
  };
4059
4232
  const toolResultsForModel = [];
4060
4233
  const richToolResults = [];
@@ -4159,11 +4332,14 @@ ${textContent}` };
4159
4332
  });
4160
4333
  } else {
4161
4334
  span?.end({ result: { value: result2.output ?? null, isError: false } });
4335
+ const serialized = JSON.stringify(result2.output ?? null);
4336
+ const outputTokenEstimate = Math.ceil(serialized.length / 4);
4162
4337
  yield pushEvent({
4163
4338
  type: "tool:completed",
4164
4339
  tool: result2.tool,
4165
4340
  output: result2.output,
4166
- duration: now() - batchStart
4341
+ duration: now() - batchStart,
4342
+ outputTokenEstimate
4167
4343
  });
4168
4344
  const { mediaItems, strippedOutput } = extractMediaFromToolOutput(result2.output);
4169
4345
  toolResultsForModel.push({
@@ -4521,6 +4697,19 @@ var InMemoryConversationStore = class {
4521
4697
  this.purgeExpired();
4522
4698
  return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
4523
4699
  }
4700
+ async listSummaries(ownerId = DEFAULT_OWNER) {
4701
+ this.purgeExpired();
4702
+ return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4703
+ conversationId: c.conversationId,
4704
+ title: c.title,
4705
+ updatedAt: c.updatedAt,
4706
+ createdAt: c.createdAt,
4707
+ ownerId: c.ownerId,
4708
+ parentConversationId: c.parentConversationId,
4709
+ messageCount: c.messages.length,
4710
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0
4711
+ }));
4712
+ }
4524
4713
  async get(conversationId) {
4525
4714
  this.purgeExpired();
4526
4715
  return this.conversations.get(conversationId);
@@ -4622,8 +4811,12 @@ var FileConversationStore = class {
4622
4811
  conversationId: conversation.conversationId,
4623
4812
  title: conversation.title,
4624
4813
  updatedAt: conversation.updatedAt,
4814
+ createdAt: conversation.createdAt,
4625
4815
  ownerId: conversation.ownerId,
4626
- fileName: entry.name
4816
+ fileName: entry.name,
4817
+ parentConversationId: conversation.parentConversationId,
4818
+ messageCount: conversation.messages.length,
4819
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0
4627
4820
  });
4628
4821
  }
4629
4822
  } catch {
@@ -4646,6 +4839,16 @@ var FileConversationStore = class {
4646
4839
  for (const conversation of parsed.conversations ?? []) {
4647
4840
  this.conversations.set(conversation.conversationId, conversation);
4648
4841
  }
4842
+ let needsRebuild = false;
4843
+ for (const entry of this.conversations.values()) {
4844
+ if (entry.messageCount === void 0) {
4845
+ needsRebuild = true;
4846
+ break;
4847
+ }
4848
+ }
4849
+ if (needsRebuild) {
4850
+ await this.rebuildIndexFromFiles();
4851
+ }
4649
4852
  } catch {
4650
4853
  await this.rebuildIndexFromFiles();
4651
4854
  }
@@ -4661,8 +4864,12 @@ var FileConversationStore = class {
4661
4864
  conversationId: conversation.conversationId,
4662
4865
  title: conversation.title,
4663
4866
  updatedAt: conversation.updatedAt,
4867
+ createdAt: conversation.createdAt,
4664
4868
  ownerId: conversation.ownerId,
4665
- fileName
4869
+ fileName,
4870
+ parentConversationId: conversation.parentConversationId,
4871
+ messageCount: conversation.messages.length,
4872
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0
4666
4873
  });
4667
4874
  await this.writeIndex();
4668
4875
  });
@@ -4680,6 +4887,19 @@ var FileConversationStore = class {
4680
4887
  }
4681
4888
  return conversations;
4682
4889
  }
4890
+ async listSummaries(ownerId = DEFAULT_OWNER) {
4891
+ await this.ensureLoaded();
4892
+ return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4893
+ conversationId: c.conversationId,
4894
+ title: c.title,
4895
+ updatedAt: c.updatedAt,
4896
+ createdAt: c.createdAt,
4897
+ ownerId: c.ownerId,
4898
+ parentConversationId: c.parentConversationId,
4899
+ messageCount: c.messageCount,
4900
+ hasPendingApprovals: c.hasPendingApprovals
4901
+ }));
4902
+ }
4683
4903
  async get(conversationId) {
4684
4904
  await this.ensureLoaded();
4685
4905
  const summary = this.conversations.get(conversationId);
@@ -4914,6 +5134,30 @@ var KeyValueConversationStoreBase = class {
4914
5134
  }
4915
5135
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
4916
5136
  }
5137
+ async listSummaries(ownerId = DEFAULT_OWNER) {
5138
+ const kv = await this.client();
5139
+ if (!kv) {
5140
+ return await this.memoryFallback.listSummaries(ownerId);
5141
+ }
5142
+ const ids = await this.getOwnerConversationIds(ownerId);
5143
+ const summaries = [];
5144
+ for (const id of ids) {
5145
+ const meta = await this.getConversationMeta(id);
5146
+ if (meta && meta.ownerId === ownerId) {
5147
+ summaries.push({
5148
+ conversationId: meta.conversationId,
5149
+ title: meta.title,
5150
+ updatedAt: meta.updatedAt,
5151
+ createdAt: meta.createdAt,
5152
+ ownerId: meta.ownerId,
5153
+ parentConversationId: meta.parentConversationId,
5154
+ messageCount: meta.messageCount,
5155
+ hasPendingApprovals: meta.hasPendingApprovals
5156
+ });
5157
+ }
5158
+ }
5159
+ return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
5160
+ }
4917
5161
  async get(conversationId) {
4918
5162
  const kv = await this.client();
4919
5163
  if (!kv) {
@@ -4963,7 +5207,11 @@ var KeyValueConversationStoreBase = class {
4963
5207
  conversationId: nextConversation.conversationId,
4964
5208
  title: nextConversation.title,
4965
5209
  updatedAt: nextConversation.updatedAt,
4966
- ownerId: nextConversation.ownerId
5210
+ createdAt: nextConversation.createdAt,
5211
+ ownerId: nextConversation.ownerId,
5212
+ parentConversationId: nextConversation.parentConversationId,
5213
+ messageCount: nextConversation.messages.length,
5214
+ hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0
4967
5215
  }),
4968
5216
  this.ttl
4969
5217
  );
@@ -5429,7 +5677,7 @@ var TelemetryEmitter = class {
5429
5677
  };
5430
5678
 
5431
5679
  // src/index.ts
5432
- import { defineTool as defineTool4 } from "@poncho-ai/sdk";
5680
+ import { defineTool as defineTool5 } from "@poncho-ai/sdk";
5433
5681
  export {
5434
5682
  AgentHarness,
5435
5683
  InMemoryConversationStore,
@@ -5452,9 +5700,10 @@ export {
5452
5700
  createModelProvider,
5453
5701
  createSkillTools,
5454
5702
  createStateStore,
5703
+ createSubagentTools,
5455
5704
  createUploadStore,
5456
5705
  createWriteTool,
5457
- defineTool4 as defineTool,
5706
+ defineTool5 as defineTool,
5458
5707
  deriveUploadKey,
5459
5708
  ensureAgentIdentity,
5460
5709
  generateAgentId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.16.0",
3
+ "version": "0.17.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.1.1"
34
+ "@poncho-ai/sdk": "1.2.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/mustache": "^4.2.6",
package/src/harness.ts CHANGED
@@ -29,6 +29,8 @@ import { addPromptCacheBreakpoints } from "./prompt-cache.js";
29
29
  import { jsonSchemaToZod } from "./schema-converter.js";
30
30
  import type { SkillMetadata } from "./skill-context.js";
31
31
  import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
32
+ import { createSubagentTools } from "./subagent-tools.js";
33
+ import type { SubagentManager } from "./subagent-manager.js";
32
34
  import { LatitudeTelemetry } from "@latitude-data/telemetry";
33
35
  import { diag, DiagLogLevel } from "@opentelemetry/api";
34
36
  import {
@@ -502,6 +504,7 @@ export class AgentHarness {
502
504
 
503
505
  private parsedAgent?: ParsedAgent;
504
506
  private mcpBridge?: LocalMcpBridge;
507
+ private subagentManager?: SubagentManager;
505
508
 
506
509
  private resolveToolAccess(toolName: string): ToolAccess {
507
510
  const tools = this.loadedConfig?.tools;
@@ -547,6 +550,21 @@ export class AgentHarness {
547
550
  }
548
551
  }
549
552
 
553
+ unregisterTools(names: string[]): void {
554
+ this.dispatcher.unregisterMany(names);
555
+ }
556
+
557
+ setSubagentManager(manager: SubagentManager): void {
558
+ this.subagentManager = manager;
559
+ this.dispatcher.registerMany(
560
+ createSubagentTools(
561
+ manager,
562
+ () => this._currentRunConversationId,
563
+ () => this._currentRunOwnerId ?? "anonymous",
564
+ ),
565
+ );
566
+ }
567
+
550
568
  private registerConfiguredBuiltInTools(config: PonchoConfig | undefined): void {
551
569
  for (const tool of createDefaultTools(this.workingDir)) {
552
570
  if (this.isToolEnabled(tool.name)) {
@@ -1060,6 +1078,8 @@ export class AgentHarness {
1060
1078
 
1061
1079
  /** Conversation ID of the currently executing run (set during run, cleared after). */
1062
1080
  private _currentRunConversationId?: string;
1081
+ /** Owner ID of the currently executing run (used by subagent tools). */
1082
+ private _currentRunOwnerId?: string;
1063
1083
 
1064
1084
  get browserSession(): unknown {
1065
1085
  return this._browserSession;
@@ -1180,8 +1200,12 @@ export class AgentHarness {
1180
1200
  }
1181
1201
  await this.refreshSkillsIfChanged();
1182
1202
 
1183
- // Track which conversation this run belongs to so browser tools resolve the right session
1203
+ // Track which conversation/owner this run belongs to so browser & subagent tools resolve correctly
1184
1204
  this._currentRunConversationId = input.conversationId;
1205
+ const ownerParam = input.parameters?.__ownerId;
1206
+ if (typeof ownerParam === "string") {
1207
+ this._currentRunOwnerId = ownerParam;
1208
+ }
1185
1209
 
1186
1210
  const agent = this.parsedAgent as ParsedAgent;
1187
1211
  const runId = `run_${randomUUID()}`;
@@ -1210,9 +1234,29 @@ export class AgentHarness {
1210
1234
  });
1211
1235
  const developmentContext =
1212
1236
  this.environment === "development" ? `\n\n${DEVELOPMENT_MODE_CONTEXT}` : "";
1237
+ const browserContext = this._browserSession
1238
+ ? `\n\n## Browser Tools
1239
+
1240
+ The user has a live browser viewport displayed alongside the conversation. They can see everything the browser shows in real time and interact with it directly (click, type, scroll, paste).
1241
+
1242
+ ### Authentication
1243
+ When a website requires authentication or credentials, do NOT ask the user to send them in the chat. Instead, navigate to the login page and let the user enter their credentials directly in the browser viewport. Wait for them to confirm they have logged in before continuing.
1244
+
1245
+ ### Session persistence
1246
+ Browser sessions (cookies, localStorage, login state) are automatically saved and restored across conversations. If the user logged into a website in a previous conversation, that session is likely still active. Try navigating directly to the authenticated page before asking the user to log in again.
1247
+
1248
+ ### Reading page content
1249
+ - Use \`browser_content\` to read the visible text on a page. This is fast and token-efficient.
1250
+ - Use \`browser_snapshot\` to get the accessibility tree with interactive element refs for clicking and typing.
1251
+ - Use \`browser_screenshot\` only when you need to see visual layout or images. Screenshots consume significantly more tokens.
1252
+ - The accessibility tree may be sparse on some pages. If \`browser_snapshot\` returns little or no content, fall back to \`browser_content\` or \`browser_screenshot\`.
1253
+
1254
+ ### Tabs and resources
1255
+ Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.`
1256
+ : "";
1213
1257
  const promptWithSkills = this.skillContextWindow
1214
- ? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}`
1215
- : `${systemPrompt}${developmentContext}`;
1258
+ ? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
1259
+ : `${systemPrompt}${developmentContext}${browserContext}`;
1216
1260
  const mainMemory = this.memoryStore
1217
1261
  ? await this.memoryStore.getMainMemory()
1218
1262
  : undefined;
@@ -1839,6 +1883,7 @@ ${boundedMainMemory.trim()}`
1839
1883
  workingDir: this.workingDir,
1840
1884
  parameters: input.parameters ?? {},
1841
1885
  abortSignal: input.abortSignal,
1886
+ conversationId: input.conversationId,
1842
1887
  };
1843
1888
 
1844
1889
  const toolResultsForModel: Array<{
@@ -1975,11 +2020,14 @@ ${boundedMainMemory.trim()}`
1975
2020
  });
1976
2021
  } else {
1977
2022
  span?.end({ result: { value: result.output ?? null, isError: false } });
2023
+ const serialized = JSON.stringify(result.output ?? null);
2024
+ const outputTokenEstimate = Math.ceil(serialized.length / 4);
1978
2025
  yield pushEvent({
1979
2026
  type: "tool:completed",
1980
2027
  tool: result.tool,
1981
2028
  output: result.output,
1982
2029
  duration: now() - batchStart,
2030
+ outputTokenEstimate,
1983
2031
  });
1984
2032
 
1985
2033
  const { mediaItems, strippedOutput } = extractMediaFromToolOutput(result.output);
package/src/index.ts CHANGED
@@ -14,5 +14,7 @@ export * from "./state.js";
14
14
  export * from "./upload-store.js";
15
15
  export * from "./telemetry.js";
16
16
  export * from "./tool-dispatcher.js";
17
+ export * from "./subagent-manager.js";
18
+ export * from "./subagent-tools.js";
17
19
  export { defineTool } from "@poncho-ai/sdk";
18
20
  export type { ToolDefinition } from "@poncho-ai/sdk";
@@ -1,14 +1,3 @@
1
- /**
2
- * Latitude telemetry integration for Vercel AI SDK
3
- *
4
- * TODO: Implement proper Vercel AI SDK telemetry integration using:
5
- * - LatitudeTelemetry.capture() wrapper around streamText()
6
- * - experimental_telemetry: { isEnabled: true } in streamText() options
7
- *
8
- * This requires @latitude-data/telemetry package which has official
9
- * Vercel AI SDK support.
10
- */
11
-
12
1
  export interface LatitudeCaptureConfig {
13
2
  apiKeyEnv?: string;
14
3
  projectIdEnv?: string;
@@ -17,8 +6,9 @@ export interface LatitudeCaptureConfig {
17
6
  }
18
7
 
19
8
  /**
20
- * Placeholder for Latitude telemetry integration
21
- * This will be properly implemented once Vercel AI SDK migration is complete
9
+ * Reads and validates Latitude telemetry configuration from environment
10
+ * variables. The actual telemetry capture is handled by LatitudeTelemetry
11
+ * from @latitude-data/telemetry in harness.ts (via runWithTelemetry).
22
12
  */
23
13
  export class LatitudeCapture {
24
14
  private readonly apiKey?: string;
package/src/state.ts CHANGED
@@ -38,12 +38,22 @@ export interface Conversation {
38
38
  }>;
39
39
  ownerId: string;
40
40
  tenantId: string | null;
41
+ contextTokens?: number;
42
+ contextWindow?: number;
43
+ parentConversationId?: string;
44
+ subagentMeta?: {
45
+ task: string;
46
+ status: "running" | "completed" | "error" | "stopped";
47
+ result?: import("@poncho-ai/sdk").RunResult;
48
+ error?: import("@poncho-ai/sdk").AgentFailure;
49
+ };
41
50
  createdAt: number;
42
51
  updatedAt: number;
43
52
  }
44
53
 
45
54
  export interface ConversationStore {
46
55
  list(ownerId?: string): Promise<Conversation[]>;
56
+ listSummaries(ownerId?: string): Promise<ConversationSummary[]>;
47
57
  get(conversationId: string): Promise<Conversation | undefined>;
48
58
  create(ownerId?: string, title?: string): Promise<Conversation>;
49
59
  update(conversation: Conversation): Promise<void>;
@@ -220,6 +230,23 @@ export class InMemoryConversationStore implements ConversationStore {
220
230
  .sort((a, b) => b.updatedAt - a.updatedAt);
221
231
  }
222
232
 
233
+ async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
234
+ this.purgeExpired();
235
+ return Array.from(this.conversations.values())
236
+ .filter((c) => c.ownerId === ownerId)
237
+ .sort((a, b) => b.updatedAt - a.updatedAt)
238
+ .map((c) => ({
239
+ conversationId: c.conversationId,
240
+ title: c.title,
241
+ updatedAt: c.updatedAt,
242
+ createdAt: c.createdAt,
243
+ ownerId: c.ownerId,
244
+ parentConversationId: c.parentConversationId,
245
+ messageCount: c.messages.length,
246
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
247
+ }));
248
+ }
249
+
223
250
  async get(conversationId: string): Promise<Conversation | undefined> {
224
251
  this.purgeExpired();
225
252
  return this.conversations.get(conversationId);
@@ -266,14 +293,29 @@ export class InMemoryConversationStore implements ConversationStore {
266
293
  }
267
294
  }
268
295
 
296
+ export type ConversationSummary = {
297
+ conversationId: string;
298
+ title: string;
299
+ updatedAt: number;
300
+ createdAt?: number;
301
+ ownerId: string;
302
+ parentConversationId?: string;
303
+ messageCount?: number;
304
+ hasPendingApprovals?: boolean;
305
+ };
306
+
269
307
  type ConversationStoreFile = {
270
308
  schemaVersion: string;
271
309
  conversations: Array<{
272
310
  conversationId: string;
273
311
  title: string;
274
312
  updatedAt: number;
313
+ createdAt?: number;
275
314
  ownerId: string;
276
315
  fileName: string;
316
+ parentConversationId?: string;
317
+ messageCount?: number;
318
+ hasPendingApprovals?: boolean;
277
319
  }>;
278
320
  };
279
321
 
@@ -342,8 +384,12 @@ class FileConversationStore implements ConversationStore {
342
384
  conversationId: conversation.conversationId,
343
385
  title: conversation.title,
344
386
  updatedAt: conversation.updatedAt,
387
+ createdAt: conversation.createdAt,
345
388
  ownerId: conversation.ownerId,
346
389
  fileName: entry.name,
390
+ parentConversationId: conversation.parentConversationId,
391
+ messageCount: conversation.messages.length,
392
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
347
393
  });
348
394
  }
349
395
  } catch {
@@ -369,6 +415,17 @@ class FileConversationStore implements ConversationStore {
369
415
  for (const conversation of parsed.conversations ?? []) {
370
416
  this.conversations.set(conversation.conversationId, conversation);
371
417
  }
418
+ // Rebuild if any entry is from an older index format (missing messageCount)
419
+ let needsRebuild = false;
420
+ for (const entry of this.conversations.values()) {
421
+ if (entry.messageCount === undefined) {
422
+ needsRebuild = true;
423
+ break;
424
+ }
425
+ }
426
+ if (needsRebuild) {
427
+ await this.rebuildIndexFromFiles();
428
+ }
372
429
  } catch {
373
430
  await this.rebuildIndexFromFiles();
374
431
  }
@@ -385,8 +442,12 @@ class FileConversationStore implements ConversationStore {
385
442
  conversationId: conversation.conversationId,
386
443
  title: conversation.title,
387
444
  updatedAt: conversation.updatedAt,
445
+ createdAt: conversation.createdAt,
388
446
  ownerId: conversation.ownerId,
389
447
  fileName,
448
+ parentConversationId: conversation.parentConversationId,
449
+ messageCount: conversation.messages.length,
450
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
390
451
  });
391
452
  await this.writeIndex();
392
453
  });
@@ -408,6 +469,23 @@ class FileConversationStore implements ConversationStore {
408
469
  return conversations;
409
470
  }
410
471
 
472
+ async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
473
+ await this.ensureLoaded();
474
+ return Array.from(this.conversations.values())
475
+ .filter((c) => c.ownerId === ownerId)
476
+ .sort((a, b) => b.updatedAt - a.updatedAt)
477
+ .map((c) => ({
478
+ conversationId: c.conversationId,
479
+ title: c.title,
480
+ updatedAt: c.updatedAt,
481
+ createdAt: c.createdAt,
482
+ ownerId: c.ownerId,
483
+ parentConversationId: c.parentConversationId,
484
+ messageCount: c.messageCount,
485
+ hasPendingApprovals: c.hasPendingApprovals,
486
+ }));
487
+ }
488
+
411
489
  async get(conversationId: string): Promise<Conversation | undefined> {
412
490
  await this.ensureLoaded();
413
491
  const summary = this.conversations.get(conversationId);
@@ -573,7 +651,11 @@ type ConversationMeta = {
573
651
  conversationId: string;
574
652
  title: string;
575
653
  updatedAt: number;
654
+ createdAt?: number;
576
655
  ownerId: string;
656
+ parentConversationId?: string;
657
+ messageCount?: number;
658
+ hasPendingApprovals?: boolean;
577
659
  };
578
660
 
579
661
  abstract class KeyValueConversationStoreBase implements ConversationStore {
@@ -690,6 +772,31 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
690
772
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
691
773
  }
692
774
 
775
+ async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
776
+ const kv = await this.client();
777
+ if (!kv) {
778
+ return await this.memoryFallback.listSummaries(ownerId);
779
+ }
780
+ const ids = await this.getOwnerConversationIds(ownerId);
781
+ const summaries: ConversationSummary[] = [];
782
+ for (const id of ids) {
783
+ const meta = await this.getConversationMeta(id);
784
+ if (meta && meta.ownerId === ownerId) {
785
+ summaries.push({
786
+ conversationId: meta.conversationId,
787
+ title: meta.title,
788
+ updatedAt: meta.updatedAt,
789
+ createdAt: meta.createdAt,
790
+ ownerId: meta.ownerId,
791
+ parentConversationId: meta.parentConversationId,
792
+ messageCount: meta.messageCount,
793
+ hasPendingApprovals: meta.hasPendingApprovals,
794
+ });
795
+ }
796
+ }
797
+ return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
798
+ }
799
+
693
800
  async get(conversationId: string): Promise<Conversation | undefined> {
694
801
  const kv = await this.client();
695
802
  if (!kv) {
@@ -741,7 +848,11 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
741
848
  conversationId: nextConversation.conversationId,
742
849
  title: nextConversation.title,
743
850
  updatedAt: nextConversation.updatedAt,
851
+ createdAt: nextConversation.createdAt,
744
852
  ownerId: nextConversation.ownerId,
853
+ parentConversationId: nextConversation.parentConversationId,
854
+ messageCount: nextConversation.messages.length,
855
+ hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
745
856
  } satisfies ConversationMeta),
746
857
  this.ttl,
747
858
  );
@@ -0,0 +1,30 @@
1
+ import type { AgentFailure, Message, RunResult } from "@poncho-ai/sdk";
2
+
3
+ export interface SubagentResult {
4
+ subagentId: string;
5
+ status: "completed" | "error" | "stopped";
6
+ latestMessages?: Message[];
7
+ result?: RunResult;
8
+ error?: AgentFailure;
9
+ }
10
+
11
+ export interface SubagentSummary {
12
+ subagentId: string;
13
+ task: string;
14
+ status: string;
15
+ messageCount: number;
16
+ }
17
+
18
+ export interface SubagentManager {
19
+ spawn(opts: {
20
+ task: string;
21
+ parentConversationId: string;
22
+ ownerId: string;
23
+ }): Promise<SubagentResult>;
24
+
25
+ sendMessage(subagentId: string, message: string): Promise<SubagentResult>;
26
+
27
+ stop(subagentId: string): Promise<void>;
28
+
29
+ list(parentConversationId: string): Promise<SubagentSummary[]>;
30
+ }
@@ -0,0 +1,160 @@
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
+ };
32
+
33
+ export const createSubagentTools = (
34
+ manager: SubagentManager,
35
+ getConversationId: () => string | undefined,
36
+ getOwnerId: () => string,
37
+ ): ToolDefinition[] => [
38
+ defineTool({
39
+ name: "spawn_subagent",
40
+ 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" +
44
+ "Guidelines:\n" +
45
+ "- Use subagents to parallelize work: call spawn_subagent multiple times in one response for independent sub-tasks -- they run concurrently.\n" +
46
+ "- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n" +
47
+ "- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ task: {
52
+ type: "string",
53
+ description:
54
+ "Thorough, self-contained instructions for the subagent. Include all relevant context, " +
55
+ "goals, and constraints -- the subagent starts with zero prior conversation history.",
56
+ },
57
+ },
58
+ required: ["task"],
59
+ additionalProperties: false,
60
+ },
61
+ handler: async (input) => {
62
+ const task = typeof input.task === "string" ? input.task : "";
63
+ if (!task.trim()) {
64
+ return { error: "task is required" };
65
+ }
66
+ const conversationId = getConversationId();
67
+ if (!conversationId) {
68
+ return { error: "no active conversation to spawn subagent from" };
69
+ }
70
+ const result = await manager.spawn({
71
+ task: task.trim(),
72
+ parentConversationId: conversationId,
73
+ ownerId: getOwnerId(),
74
+ });
75
+ return summarizeResult(result);
76
+ },
77
+ }),
78
+
79
+ defineTool({
80
+ name: "message_subagent",
81
+ 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. " +
84
+ "Only works when the subagent is not currently running.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ subagent_id: {
89
+ type: "string",
90
+ description: "The subagent ID (from spawn_subagent result or list_subagents).",
91
+ },
92
+ message: {
93
+ type: "string",
94
+ description: "The follow-up instructions or message to send.",
95
+ },
96
+ },
97
+ required: ["subagent_id", "message"],
98
+ additionalProperties: false,
99
+ },
100
+ handler: async (input) => {
101
+ const subagentId = typeof input.subagent_id === "string" ? input.subagent_id : "";
102
+ const message = typeof input.message === "string" ? input.message : "";
103
+ if (!subagentId || !message.trim()) {
104
+ return { error: "subagent_id and message are required" };
105
+ }
106
+ const result = await manager.sendMessage(subagentId, message.trim());
107
+ return summarizeResult(result);
108
+ },
109
+ }),
110
+
111
+ defineTool({
112
+ name: "stop_subagent",
113
+ description:
114
+ "Stop a running subagent. The subagent's conversation is preserved but it will stop processing. " +
115
+ "Use this to cancel work that is no longer needed.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ subagent_id: {
120
+ type: "string",
121
+ description: "The subagent ID (from spawn_subagent result or list_subagents).",
122
+ },
123
+ },
124
+ required: ["subagent_id"],
125
+ additionalProperties: false,
126
+ },
127
+ handler: async (input) => {
128
+ const subagentId = typeof input.subagent_id === "string" ? input.subagent_id : "";
129
+ if (!subagentId) {
130
+ return { error: "subagent_id is required" };
131
+ }
132
+ await manager.stop(subagentId);
133
+ return { message: `Subagent "${subagentId}" has been stopped.` };
134
+ },
135
+ }),
136
+
137
+ defineTool({
138
+ name: "list_subagents",
139
+ description:
140
+ "List all subagents that have been spawned in this conversation. Returns each subagent's ID, " +
141
+ "original task, current status, and message count. Use this to look up subagent IDs before " +
142
+ "calling message_subagent or stop_subagent.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {},
146
+ additionalProperties: false,
147
+ },
148
+ handler: async () => {
149
+ const conversationId = getConversationId();
150
+ if (!conversationId) {
151
+ return { error: "no active conversation" };
152
+ }
153
+ const subagents = await manager.list(conversationId);
154
+ if (subagents.length === 0) {
155
+ return { message: "No subagents have been spawned in this conversation." };
156
+ }
157
+ return { subagents };
158
+ },
159
+ }),
160
+ ];
@@ -1,6 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.11.2 lint /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
- > eslint src/
4
-
5
- sh: eslint: command not found
6
-  ELIFECYCLE  Command failed.
@@ -1,139 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.14.2 test /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
- > vitest
4
-
5
-
6
-  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/harness
7
-
8
- [event] step:completed {"type":"step:completed","step":1,"duration":1}
9
- ✓ test/telemetry.test.ts  (3 tests) 3ms
10
- [event] step:started {"type":"step:started","step":2}
11
- ✓ test/schema-converter.test.ts  (27 tests) 16ms
12
- stdout | test/mcp.test.ts > mcp bridge protocol transports > discovers and calls tools over streamable HTTP
13
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
14
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
15
-
16
- ✓ test/agent-parser.test.ts  (10 tests) 17ms
17
- ✓ test/memory.test.ts  (4 tests) 16ms
18
- stdout | test/mcp.test.ts > mcp bridge protocol transports > selects discovered tools by requested patterns
19
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
20
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
21
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
22
-
23
- stdout | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
24
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":0,"filteredByPolicyCount":0,"filteredByIntentCount":0}
25
-
26
- stderr | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
27
- [poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
28
-
29
- stdout | test/mcp.test.ts > mcp bridge protocol transports > returns actionable errors for 403 permission failures
30
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
31
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
32
-
33
- ✓ test/mcp.test.ts  (6 tests) 97ms
34
- ✓ test/state.test.ts  (5 tests) 217ms
35
- ✓ test/agent-identity.test.ts  (2 tests) 15ms
36
- ✓ test/model-factory.test.ts  (4 tests) 3ms
37
- stdout | test/harness.test.ts > agent harness > registers default filesystem tools
38
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
39
-
40
- stdout | test/harness.test.ts > agent harness > disables write_file by default in production environment
41
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
42
-
43
- stdout | test/harness.test.ts > agent harness > allows disabling built-in tools via poncho.config.js
44
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
45
-
46
- stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
47
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
48
-
49
- stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
50
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
51
-
52
- stdout | test/harness.test.ts > agent harness > does not auto-register exported tool objects from skill scripts
53
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
54
-
55
- stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
56
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
57
-
58
- stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
59
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
60
-
61
- stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
62
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:beta","requestedPatterns":[]}
63
-
64
- stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
65
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
66
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:obsolete","requestedPatterns":[]}
67
-
68
- stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
69
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
70
-
71
- stdout | test/harness.test.ts > agent harness > does not refresh skills outside development mode
72
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
73
-
74
- stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
75
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
76
- [poncho][mcp] {"event":"tools.cleared","reason":"activate:alpha","requestedPatterns":[]}
77
-
78
- stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
79
- [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
80
-
81
- stdout | test/harness.test.ts > agent harness > lists skill scripts through list_skill_scripts
82
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
83
-
84
- stdout | test/harness.test.ts > agent harness > runs JavaScript/TypeScript skill scripts through run_skill_script
85
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
86
-
87
- stdout | test/harness.test.ts > agent harness > runs AGENT-scope scripts from root scripts directory
88
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
89
-
90
- stdout | test/harness.test.ts > agent harness > blocks path traversal in run_skill_script
91
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
92
-
93
- stdout | test/harness.test.ts > agent harness > requires allowed-tools entries for non-standard script directories
94
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
95
-
96
- stdout | test/harness.test.ts > agent harness > registers MCP tools dynamically for stacked active skills and supports deactivation
97
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
98
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
99
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
100
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-a","requestedPatterns":["remote/a"],"registeredCount":1,"activeSkills":["skill-a"]}
101
-
102
- stdout | test/harness.test.ts > agent harness > registers MCP tools dynamically for stacked active skills and supports deactivation
103
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":2,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
104
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-b","requestedPatterns":["remote/a","remote/b"],"registeredCount":2,"activeSkills":["skill-a","skill-b"]}
105
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
106
- [poncho][mcp] {"event":"tools.refreshed","reason":"deactivate:skill-a","requestedPatterns":["remote/b"],"registeredCount":1,"activeSkills":["skill-b"]}
107
-
108
- stdout | test/harness.test.ts > agent harness > supports flat tool access config format
109
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
110
-
111
- stdout | test/harness.test.ts > agent harness > flat tool access takes priority over legacy defaults
112
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
113
-
114
- stdout | test/harness.test.ts > agent harness > byEnvironment overrides flat tool access
115
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
116
-
117
- stdout | test/harness.test.ts > agent harness > registerTools skips tools disabled via config
118
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
119
-
120
- stdout | test/harness.test.ts > agent harness > approval access level registers the tool but marks it for approval
121
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
122
-
123
- stdout | test/harness.test.ts > agent harness > tools without approval config do not require approval
124
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
125
-
126
- stdout | test/harness.test.ts > agent harness > allows in-flight MCP calls to finish after skill deactivation
127
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
128
- [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
129
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
130
- [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-slow","requestedPatterns":["remote/slow"],"registeredCount":1,"activeSkills":["skill-slow"]}
131
- [poncho][mcp] {"event":"tools.cleared","reason":"deactivate:skill-slow","requestedPatterns":[]}
132
-
133
- ✓ test/harness.test.ts  (25 tests) 365ms
134
-
135
-  Test Files  9 passed (9)
136
-  Tests  86 passed (86)
137
-  Start at  13:30:36
138
-  Duration  2.19s (transform 1.11s, setup 0ms, collect 2.81s, tests 749ms, environment 5ms, prepare 1.23s)
139
-