@poncho-ai/harness 0.29.0 → 0.30.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.29.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.30.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 297.56 KB
12
- ESM ⚡️ Build success in 32ms
11
+ ESM dist/index.js 300.00 KB
12
+ ESM ⚡️ Build success in 135ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 4608ms
15
- DTS dist/index.d.ts 30.41 KB
14
+ DTS ⚡️ Build success in 7526ms
15
+ DTS dist/index.d.ts 30.64 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`193c367`](https://github.com/cesr/poncho-ai/commit/193c367568dce22a470dff6acd022c221be3b722) Thanks [@cesr](https://github.com/cesr)! - Unified continuation logic across all entry points (chat, cron, subagents, SDK) with mid-stream soft deadline checkpointing and proper context preservation across continuation boundaries.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`193c367`](https://github.com/cesr/poncho-ai/commit/193c367568dce22a470dff6acd022c221be3b722)]:
12
+ - @poncho-ai/sdk@1.6.3
13
+
3
14
  ## 0.29.0
4
15
 
5
16
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -176,6 +176,10 @@ interface Conversation {
176
176
  /** Harness-internal message chain preserved across continuation runs.
177
177
  * Cleared when a run completes without continuation. */
178
178
  _continuationMessages?: Message[];
179
+ /** Number of continuation pickups for the current multi-step run.
180
+ * Reset when a run completes without continuation. Used to enforce
181
+ * a maximum continuation count across all entry points. */
182
+ _continuationCount?: number;
179
183
  /** Full structured message chain from the last harness run, including
180
184
  * tool-call and tool-result messages the model needs for context.
181
185
  * Unlike `_continuationMessages`, this is always set after a run
package/dist/index.js CHANGED
@@ -6131,7 +6131,7 @@ ${this.skillFingerprint}`;
6131
6131
  if (lastMsg && lastMsg.role !== "user") {
6132
6132
  messages.push({
6133
6133
  role: "user",
6134
- 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.]",
6134
+ content: "[System: Your previous turn was interrupted by a time limit. Your partial response above is already visible to the user. Continue EXACTLY from where you left off \u2014 do NOT restart, re-summarize, or repeat any content you already produced. If you were mid-sentence or mid-table, continue that sentence or table. Proceed directly with the next action or output.]",
6135
6135
  metadata: { timestamp: now(), id: randomUUID3() }
6136
6136
  });
6137
6137
  }
@@ -6461,7 +6461,10 @@ ${textContent}` };
6461
6461
  let chunkCount = 0;
6462
6462
  const hasRunTimeout = timeoutMs > 0;
6463
6463
  const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
6464
+ const hasSoftDeadline = softDeadlineMs > 0;
6465
+ const INTER_CHUNK_TIMEOUT_MS = 6e4;
6464
6466
  const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
6467
+ let softDeadlineFiredDuringStream = false;
6465
6468
  try {
6466
6469
  while (true) {
6467
6470
  if (isCancelled()) {
@@ -6469,8 +6472,8 @@ ${textContent}` };
6469
6472
  return;
6470
6473
  }
6471
6474
  if (hasRunTimeout) {
6472
- const remaining2 = streamDeadline - now();
6473
- if (remaining2 <= 0) {
6475
+ const remaining = streamDeadline - now();
6476
+ if (remaining <= 0) {
6474
6477
  yield pushEvent({
6475
6478
  type: "run:error",
6476
6479
  runId,
@@ -6485,22 +6488,33 @@ ${textContent}` };
6485
6488
  return;
6486
6489
  }
6487
6490
  }
6488
- const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
6489
- const timeout = chunkCount === 0 ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS) : hasRunTimeout ? remaining : 0;
6491
+ if (hasSoftDeadline && chunkCount > 0 && now() - start >= softDeadlineMs) {
6492
+ softDeadlineFiredDuringStream = true;
6493
+ break;
6494
+ }
6495
+ const hardRemaining = hasRunTimeout ? streamDeadline - now() : Infinity;
6496
+ const softRemaining = hasSoftDeadline ? Math.max(0, start + softDeadlineMs - now()) : Infinity;
6497
+ const deadlineRemaining = Math.min(hardRemaining, softRemaining);
6498
+ const timeout = chunkCount === 0 ? Math.min(deadlineRemaining, FIRST_CHUNK_TIMEOUT_MS) : Math.min(deadlineRemaining, INTER_CHUNK_TIMEOUT_MS);
6490
6499
  let nextPart;
6491
- if (timeout <= 0 && chunkCount > 0) {
6500
+ if (timeout <= 0 && chunkCount > 0 && !hasSoftDeadline) {
6492
6501
  nextPart = await fullStreamIterator.next();
6493
6502
  } else {
6503
+ const effectiveTimeout = Math.max(timeout, 1);
6494
6504
  let timer;
6495
6505
  nextPart = await Promise.race([
6496
6506
  fullStreamIterator.next(),
6497
6507
  new Promise((resolve12) => {
6498
- timer = setTimeout(() => resolve12(null), timeout);
6508
+ timer = setTimeout(() => resolve12(null), effectiveTimeout);
6499
6509
  })
6500
6510
  ]);
6501
6511
  clearTimeout(timer);
6502
6512
  }
6503
6513
  if (nextPart === null) {
6514
+ if (hasSoftDeadline && deadlineRemaining <= INTER_CHUNK_TIMEOUT_MS) {
6515
+ softDeadlineFiredDuringStream = true;
6516
+ break;
6517
+ }
6504
6518
  const isFirstChunk = chunkCount === 0;
6505
6519
  console.error(
6506
6520
  `[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`
@@ -6533,11 +6547,42 @@ ${textContent}` };
6533
6547
  fullStreamIterator.return?.(void 0)?.catch?.(() => {
6534
6548
  });
6535
6549
  }
6550
+ if (softDeadlineFiredDuringStream) {
6551
+ if (fullText.length > 0) {
6552
+ messages.push({
6553
+ role: "assistant",
6554
+ content: fullText,
6555
+ metadata: { timestamp: now(), id: randomUUID3(), step }
6556
+ });
6557
+ }
6558
+ const result_ = {
6559
+ status: "completed",
6560
+ response: responseText + fullText,
6561
+ steps: step,
6562
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6563
+ duration: now() - start,
6564
+ continuation: true,
6565
+ continuationMessages: [...messages],
6566
+ maxSteps,
6567
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
6568
+ contextWindow
6569
+ };
6570
+ console.info(`[poncho][harness] Soft deadline fired mid-stream at step ${step} (${(now() - start).toFixed(0)}ms). Checkpointing with ${fullText.length} chars of partial text.`);
6571
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
6572
+ return;
6573
+ }
6536
6574
  if (isCancelled()) {
6537
6575
  yield emitCancellation();
6538
6576
  return;
6539
6577
  }
6540
6578
  if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
6579
+ if (fullText.length > 0) {
6580
+ messages.push({
6581
+ role: "assistant",
6582
+ content: fullText,
6583
+ metadata: { timestamp: now(), id: randomUUID3(), step }
6584
+ });
6585
+ }
6541
6586
  const result_ = {
6542
6587
  status: "completed",
6543
6588
  response: responseText + fullText,
@@ -6789,6 +6834,13 @@ ${textContent}` };
6789
6834
  batchResults = await this.dispatcher.executeBatch(approvedCalls, toolContext);
6790
6835
  }
6791
6836
  if (batchResults === TOOL_DEADLINE_SENTINEL) {
6837
+ if (fullText.length > 0) {
6838
+ messages.push({
6839
+ role: "assistant",
6840
+ content: fullText,
6841
+ metadata: { timestamp: now(), id: randomUUID3(), step }
6842
+ });
6843
+ }
6792
6844
  const result_ = {
6793
6845
  status: "completed",
6794
6846
  response: responseText + fullText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "redis": "^5.10.0",
35
35
  "yaml": "^2.4.0",
36
36
  "zod": "^3.22.0",
37
- "@poncho-ai/sdk": "1.6.2"
37
+ "@poncho-ai/sdk": "1.6.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/mustache": "^4.2.6",
package/src/harness.ts CHANGED
@@ -1643,7 +1643,7 @@ ${boundedMainMemory.trim()}`
1643
1643
  if (lastMsg && lastMsg.role !== "user") {
1644
1644
  messages.push({
1645
1645
  role: "user",
1646
- content: "[System: Your previous turn was interrupted by a time limit. Continue from where you left off — do NOT repeat what you already said. Proceed directly with the next action or tool call.]",
1646
+ content: "[System: Your previous turn was interrupted by a time limit. Your partial response above is already visible to the user. Continue EXACTLY from where you left off — do NOT restart, re-summarize, or repeat any content you already produced. If you were mid-sentence or mid-table, continue that sentence or table. Proceed directly with the next action or output.]",
1647
1647
  metadata: { timestamp: now(), id: randomUUID() },
1648
1648
  });
1649
1649
  }
@@ -2048,7 +2048,10 @@ ${boundedMainMemory.trim()}`
2048
2048
  let chunkCount = 0;
2049
2049
  const hasRunTimeout = timeoutMs > 0;
2050
2050
  const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
2051
+ const hasSoftDeadline = softDeadlineMs > 0;
2052
+ const INTER_CHUNK_TIMEOUT_MS = 60_000;
2051
2053
  const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
2054
+ let softDeadlineFiredDuringStream = false;
2052
2055
  try {
2053
2056
  while (true) {
2054
2057
  if (isCancelled()) {
@@ -2072,25 +2075,36 @@ ${boundedMainMemory.trim()}`
2072
2075
  return;
2073
2076
  }
2074
2077
  }
2075
- const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
2078
+ if (hasSoftDeadline && chunkCount > 0 && now() - start >= softDeadlineMs) {
2079
+ softDeadlineFiredDuringStream = true;
2080
+ break;
2081
+ }
2082
+ const hardRemaining = hasRunTimeout ? streamDeadline - now() : Infinity;
2083
+ const softRemaining = hasSoftDeadline ? Math.max(0, (start + softDeadlineMs) - now()) : Infinity;
2084
+ const deadlineRemaining = Math.min(hardRemaining, softRemaining);
2076
2085
  const timeout = chunkCount === 0
2077
- ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS)
2078
- : hasRunTimeout ? remaining : 0;
2086
+ ? Math.min(deadlineRemaining, FIRST_CHUNK_TIMEOUT_MS)
2087
+ : Math.min(deadlineRemaining, INTER_CHUNK_TIMEOUT_MS);
2079
2088
  let nextPart: IteratorResult<(typeof result.fullStream) extends AsyncIterable<infer T> ? T : never> | null;
2080
- if (timeout <= 0 && chunkCount > 0) {
2089
+ if (timeout <= 0 && chunkCount > 0 && !hasSoftDeadline) {
2081
2090
  nextPart = await fullStreamIterator.next();
2082
2091
  } else {
2092
+ const effectiveTimeout = Math.max(timeout, 1);
2083
2093
  let timer: ReturnType<typeof setTimeout> | undefined;
2084
2094
  nextPart = await Promise.race([
2085
2095
  fullStreamIterator.next(),
2086
2096
  new Promise<null>((resolve) => {
2087
- timer = setTimeout(() => resolve(null), timeout);
2097
+ timer = setTimeout(() => resolve(null), effectiveTimeout);
2088
2098
  }),
2089
2099
  ]);
2090
2100
  clearTimeout(timer);
2091
2101
  }
2092
2102
 
2093
2103
  if (nextPart === null) {
2104
+ if (hasSoftDeadline && deadlineRemaining <= INTER_CHUNK_TIMEOUT_MS) {
2105
+ softDeadlineFiredDuringStream = true;
2106
+ break;
2107
+ }
2094
2108
  const isFirstChunk = chunkCount === 0;
2095
2109
  console.error(
2096
2110
  `[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`,
@@ -2125,6 +2139,31 @@ ${boundedMainMemory.trim()}`
2125
2139
  fullStreamIterator.return?.(undefined)?.catch?.(() => {});
2126
2140
  }
2127
2141
 
2142
+ if (softDeadlineFiredDuringStream) {
2143
+ if (fullText.length > 0) {
2144
+ messages.push({
2145
+ role: "assistant",
2146
+ content: fullText,
2147
+ metadata: { timestamp: now(), id: randomUUID(), step },
2148
+ });
2149
+ }
2150
+ const result_: RunResult = {
2151
+ status: "completed",
2152
+ response: responseText + fullText,
2153
+ steps: step,
2154
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
2155
+ duration: now() - start,
2156
+ continuation: true,
2157
+ continuationMessages: [...messages],
2158
+ maxSteps,
2159
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
2160
+ contextWindow,
2161
+ };
2162
+ console.info(`[poncho][harness] Soft deadline fired mid-stream at step ${step} (${(now() - start).toFixed(0)}ms). Checkpointing with ${fullText.length} chars of partial text.`);
2163
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
2164
+ return;
2165
+ }
2166
+
2128
2167
  if (isCancelled()) {
2129
2168
  yield emitCancellation();
2130
2169
  return;
@@ -2133,6 +2172,13 @@ ${boundedMainMemory.trim()}`
2133
2172
  // Post-streaming soft deadline: if the model stream took long enough to
2134
2173
  // push past the soft deadline, checkpoint now before tool execution.
2135
2174
  if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
2175
+ if (fullText.length > 0) {
2176
+ messages.push({
2177
+ role: "assistant",
2178
+ content: fullText,
2179
+ metadata: { timestamp: now(), id: randomUUID(), step },
2180
+ });
2181
+ }
2136
2182
  const result_: RunResult = {
2137
2183
  status: "completed",
2138
2184
  response: responseText + fullText,
@@ -2446,6 +2492,13 @@ ${boundedMainMemory.trim()}`
2446
2492
  }
2447
2493
 
2448
2494
  if ((batchResults as unknown) === TOOL_DEADLINE_SENTINEL) {
2495
+ if (fullText.length > 0) {
2496
+ messages.push({
2497
+ role: "assistant",
2498
+ content: fullText,
2499
+ metadata: { timestamp: now(), id: randomUUID(), step },
2500
+ });
2501
+ }
2449
2502
  const result_: RunResult = {
2450
2503
  status: "completed",
2451
2504
  response: responseText + fullText,
package/src/state.ts CHANGED
@@ -71,6 +71,10 @@ export interface Conversation {
71
71
  /** Harness-internal message chain preserved across continuation runs.
72
72
  * Cleared when a run completes without continuation. */
73
73
  _continuationMessages?: Message[];
74
+ /** Number of continuation pickups for the current multi-step run.
75
+ * Reset when a run completes without continuation. Used to enforce
76
+ * a maximum continuation count across all entry points. */
77
+ _continuationCount?: number;
74
78
  /** Full structured message chain from the last harness run, including
75
79
  * tool-call and tool-result messages the model needs for context.
76
80
  * Unlike `_continuationMessages`, this is always set after a run
@@ -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,34 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.26.0 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
- stdout | test/mcp.test.ts > mcp bridge protocol transports > discovers and calls tools over streamable HTTP
9
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
10
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
11
-
12
- stdout | test/mcp.test.ts > mcp bridge protocol transports > sends custom headers alongside bearer token
13
- [poncho][mcp] {"event":"catalog.loaded","server":"custom-headers","discoveredCount":1}
14
-
15
- stderr | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
16
- stdout | test/mcp.test.ts > mcp bridge protocol transports > selects discovered tools by requested patterns
17
- [poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
18
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
19
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
20
-
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
- [event] step:completed {"type":"step:completed","step":1,"duration":1}
27
- ✓ test/telemetry.test.ts  (3 tests) 5ms
28
- [event] step:started {"type":"step:started","step":2}
29
- ✓ test/schema-converter.test.ts  (27 tests) 13ms
30
- stdout | test/mcp.test.ts > mcp bridge protocol transports > returns actionable errors for 403 permission failures
31
- [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
32
- [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
33
-
34
- ✓ test/mcp.test.ts  (7 tests) 84ms