@poncho-ai/harness 0.8.0 → 0.9.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.8.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.9.0 build /Users/cesar/Dev/latitude/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 130.60 KB
11
- ESM ⚡️ Build success in 39ms
10
+ ESM dist/index.js 133.06 KB
11
+ ESM ⚡️ Build success in 29ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 3563ms
13
+ DTS ⚡️ Build success in 3021ms
14
14
  DTS dist/index.d.ts 17.17 KB
package/dist/index.js CHANGED
@@ -2032,6 +2032,13 @@ var ToolDispatcher = class {
2032
2032
  return this.tools.get(name);
2033
2033
  }
2034
2034
  async execute(call, context) {
2035
+ if (context.abortSignal?.aborted) {
2036
+ return {
2037
+ callId: call.id,
2038
+ tool: call.name,
2039
+ error: "Tool execution cancelled"
2040
+ };
2041
+ }
2035
2042
  const definition = this.tools.get(call.name);
2036
2043
  if (!definition) {
2037
2044
  return {
@@ -2042,12 +2049,26 @@ var ToolDispatcher = class {
2042
2049
  }
2043
2050
  try {
2044
2051
  const output = await definition.handler(call.input, context);
2052
+ if (context.abortSignal?.aborted) {
2053
+ return {
2054
+ callId: call.id,
2055
+ tool: call.name,
2056
+ error: "Tool execution cancelled"
2057
+ };
2058
+ }
2045
2059
  return {
2046
2060
  callId: call.id,
2047
2061
  tool: call.name,
2048
2062
  output
2049
2063
  };
2050
2064
  } catch (error) {
2065
+ if (context.abortSignal?.aborted) {
2066
+ return {
2067
+ callId: call.id,
2068
+ tool: call.name,
2069
+ error: "Tool execution cancelled"
2070
+ };
2071
+ }
2051
2072
  return {
2052
2073
  callId: call.id,
2053
2074
  tool: call.name,
@@ -2056,7 +2077,19 @@ var ToolDispatcher = class {
2056
2077
  }
2057
2078
  }
2058
2079
  async executeBatch(calls, context) {
2059
- return Promise.all(calls.map(async (call) => this.execute(call, context)));
2080
+ const results = [];
2081
+ for (const call of calls) {
2082
+ if (context.abortSignal?.aborted) {
2083
+ results.push({
2084
+ callId: call.id,
2085
+ tool: call.name,
2086
+ error: "Tool execution cancelled"
2087
+ });
2088
+ continue;
2089
+ }
2090
+ results.push(await this.execute(call, context));
2091
+ }
2092
+ return results;
2060
2093
  }
2061
2094
  };
2062
2095
 
@@ -2072,6 +2105,17 @@ var SKILL_TOOL_NAMES = [
2072
2105
  "run_skill_script"
2073
2106
  ];
2074
2107
  var trimMessageWindow = (messages) => messages.length <= MAX_CONTEXT_MESSAGES ? messages : messages.slice(messages.length - MAX_CONTEXT_MESSAGES);
2108
+ var isAbortError = (error) => {
2109
+ if (!error || typeof error !== "object") {
2110
+ return false;
2111
+ }
2112
+ const maybeName = "name" in error ? String(error.name ?? "") : "";
2113
+ if (maybeName === "AbortError") {
2114
+ return true;
2115
+ }
2116
+ const maybeMessage = "message" in error ? String(error.message ?? "") : "";
2117
+ return maybeMessage.toLowerCase().includes("abort");
2118
+ };
2075
2119
  var MODEL_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
2076
2120
  var toProviderSafeToolName = (originalName, index, used) => {
2077
2121
  if (MODEL_TOOL_NAME_PATTERN.test(originalName) && !used.has(originalName)) {
@@ -2582,6 +2626,12 @@ ${boundedMainMemory.trim()}` : "";
2582
2626
  events.push(event);
2583
2627
  return event;
2584
2628
  };
2629
+ const isCancelled = () => input.abortSignal?.aborted === true;
2630
+ let cancellationEmitted = false;
2631
+ const emitCancellation = () => {
2632
+ cancellationEmitted = true;
2633
+ return pushEvent({ type: "run:cancelled", runId });
2634
+ };
2585
2635
  yield pushEvent({
2586
2636
  type: "run:started",
2587
2637
  runId,
@@ -2597,6 +2647,10 @@ ${boundedMainMemory.trim()}` : "";
2597
2647
  let totalOutputTokens = 0;
2598
2648
  for (let step = 1; step <= maxSteps; step += 1) {
2599
2649
  try {
2650
+ if (isCancelled()) {
2651
+ yield emitCancellation();
2652
+ return;
2653
+ }
2600
2654
  if (now() - start > timeoutMs) {
2601
2655
  yield pushEvent({
2602
2656
  type: "run:error",
@@ -2690,6 +2744,7 @@ ${boundedMainMemory.trim()}` : "";
2690
2744
  messages: coreMessages,
2691
2745
  tools,
2692
2746
  temperature,
2747
+ abortSignal: input.abortSignal,
2693
2748
  ...typeof maxTokens === "number" ? { maxTokens } : {},
2694
2749
  experimental_telemetry: {
2695
2750
  isEnabled: telemetryEnabled && !!latitudeApiKey
@@ -2698,10 +2753,18 @@ ${boundedMainMemory.trim()}` : "";
2698
2753
  let streamedAnyChunk = false;
2699
2754
  let fullText = "";
2700
2755
  for await (const chunk of result.textStream) {
2756
+ if (isCancelled()) {
2757
+ yield emitCancellation();
2758
+ return;
2759
+ }
2701
2760
  streamedAnyChunk = true;
2702
2761
  fullText += chunk;
2703
2762
  yield pushEvent({ type: "model:chunk", content: chunk });
2704
2763
  }
2764
+ if (isCancelled()) {
2765
+ yield emitCancellation();
2766
+ return;
2767
+ }
2705
2768
  const fullResult = await result.response;
2706
2769
  const usage = await result.usage;
2707
2770
  const toolCallsResult = await result.toolCalls;
@@ -2746,11 +2809,16 @@ ${boundedMainMemory.trim()}` : "";
2746
2809
  agentId: agent.frontmatter.id ?? agent.frontmatter.name,
2747
2810
  step,
2748
2811
  workingDir: this.workingDir,
2749
- parameters: input.parameters ?? {}
2812
+ parameters: input.parameters ?? {},
2813
+ abortSignal: input.abortSignal
2750
2814
  };
2751
2815
  const toolResultsForModel = [];
2752
2816
  const approvedCalls = [];
2753
2817
  for (const call of toolCalls) {
2818
+ if (isCancelled()) {
2819
+ yield emitCancellation();
2820
+ return;
2821
+ }
2754
2822
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
2755
2823
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
2756
2824
  const requiresApproval = this.requiresApprovalForToolCall(
@@ -2772,6 +2840,10 @@ ${boundedMainMemory.trim()}` : "";
2772
2840
  step,
2773
2841
  approvalId
2774
2842
  }) : false;
2843
+ if (isCancelled()) {
2844
+ yield emitCancellation();
2845
+ return;
2846
+ }
2775
2847
  if (!approved) {
2776
2848
  yield pushEvent({
2777
2849
  type: "tool:approval:denied",
@@ -2801,7 +2873,15 @@ ${boundedMainMemory.trim()}` : "";
2801
2873
  });
2802
2874
  }
2803
2875
  const batchStart = now();
2876
+ if (isCancelled()) {
2877
+ yield emitCancellation();
2878
+ return;
2879
+ }
2804
2880
  const batchResults = approvedCalls.length > 0 ? await this.dispatcher.executeBatch(approvedCalls, toolContext) : [];
2881
+ if (isCancelled()) {
2882
+ yield emitCancellation();
2883
+ return;
2884
+ }
2805
2885
  for (const result2 of batchResults) {
2806
2886
  if (result2.error) {
2807
2887
  yield pushEvent({
@@ -2855,6 +2935,12 @@ ${boundedMainMemory.trim()}` : "";
2855
2935
  duration: now() - stepStart
2856
2936
  });
2857
2937
  } catch (error) {
2938
+ if (isCancelled() || isAbortError(error)) {
2939
+ if (!cancellationEmitted) {
2940
+ yield emitCancellation();
2941
+ }
2942
+ return;
2943
+ }
2858
2944
  yield pushEvent({
2859
2945
  type: "run:error",
2860
2946
  runId,
@@ -2903,6 +2989,14 @@ ${boundedMainMemory.trim()}` : "";
2903
2989
  duration: 0
2904
2990
  };
2905
2991
  }
2992
+ if (event.type === "run:cancelled") {
2993
+ finalResult = {
2994
+ status: "cancelled",
2995
+ steps: 0,
2996
+ tokens: { input: 0, output: 0, cached: 0 },
2997
+ duration: 0
2998
+ };
2999
+ }
2906
3000
  }
2907
3001
  return {
2908
3002
  runId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  "redis": "^5.10.0",
31
31
  "yaml": "^2.4.0",
32
32
  "zod": "^3.22.0",
33
- "@poncho-ai/sdk": "0.3.0"
33
+ "@poncho-ai/sdk": "0.4.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/mustache": "^4.2.6",
package/src/harness.ts CHANGED
@@ -69,6 +69,18 @@ const trimMessageWindow = (messages: Message[]): Message[] =>
69
69
  ? messages
70
70
  : messages.slice(messages.length - MAX_CONTEXT_MESSAGES);
71
71
 
72
+ const isAbortError = (error: unknown): boolean => {
73
+ if (!error || typeof error !== "object") {
74
+ return false;
75
+ }
76
+ const maybeName = "name" in error ? String(error.name ?? "") : "";
77
+ if (maybeName === "AbortError") {
78
+ return true;
79
+ }
80
+ const maybeMessage = "message" in error ? String(error.message ?? "") : "";
81
+ return maybeMessage.toLowerCase().includes("abort");
82
+ };
83
+
72
84
  const MODEL_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
73
85
 
74
86
  const toProviderSafeToolName = (
@@ -658,6 +670,12 @@ ${boundedMainMemory.trim()}`
658
670
  events.push(event);
659
671
  return event;
660
672
  };
673
+ const isCancelled = (): boolean => input.abortSignal?.aborted === true;
674
+ let cancellationEmitted = false;
675
+ const emitCancellation = (): AgentEvent => {
676
+ cancellationEmitted = true;
677
+ return pushEvent({ type: "run:cancelled", runId });
678
+ };
661
679
 
662
680
  yield pushEvent({
663
681
  type: "run:started",
@@ -677,6 +695,10 @@ ${boundedMainMemory.trim()}`
677
695
 
678
696
  for (let step = 1; step <= maxSteps; step += 1) {
679
697
  try {
698
+ if (isCancelled()) {
699
+ yield emitCancellation();
700
+ return;
701
+ }
680
702
  if (now() - start > timeoutMs) {
681
703
  yield pushEvent({
682
704
  type: "run:error",
@@ -793,6 +815,7 @@ ${boundedMainMemory.trim()}`
793
815
  messages: coreMessages,
794
816
  tools,
795
817
  temperature,
818
+ abortSignal: input.abortSignal,
796
819
  ...(typeof maxTokens === "number" ? { maxTokens } : {}),
797
820
  experimental_telemetry: {
798
821
  isEnabled: telemetryEnabled && !!latitudeApiKey,
@@ -803,10 +826,19 @@ ${boundedMainMemory.trim()}`
803
826
  let streamedAnyChunk = false;
804
827
  let fullText = "";
805
828
  for await (const chunk of result.textStream) {
806
- streamedAnyChunk = true;
807
- fullText += chunk;
808
- yield pushEvent({ type: "model:chunk", content: chunk });
809
- }
829
+ if (isCancelled()) {
830
+ yield emitCancellation();
831
+ return;
832
+ }
833
+ streamedAnyChunk = true;
834
+ fullText += chunk;
835
+ yield pushEvent({ type: "model:chunk", content: chunk });
836
+ }
837
+
838
+ if (isCancelled()) {
839
+ yield emitCancellation();
840
+ return;
841
+ }
810
842
 
811
843
  // Get full response with usage and tool calls
812
844
  const fullResult = await result.response;
@@ -861,6 +893,7 @@ ${boundedMainMemory.trim()}`
861
893
  step,
862
894
  workingDir: this.workingDir,
863
895
  parameters: input.parameters ?? {},
896
+ abortSignal: input.abortSignal,
864
897
  };
865
898
 
866
899
  const toolResultsForModel: Array<{
@@ -877,6 +910,10 @@ ${boundedMainMemory.trim()}`
877
910
  }> = [];
878
911
 
879
912
  for (const call of toolCalls) {
913
+ if (isCancelled()) {
914
+ yield emitCancellation();
915
+ return;
916
+ }
880
917
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
881
918
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
882
919
  const requiresApproval = this.requiresApprovalForToolCall(
@@ -900,6 +937,10 @@ ${boundedMainMemory.trim()}`
900
937
  approvalId,
901
938
  })
902
939
  : false;
940
+ if (isCancelled()) {
941
+ yield emitCancellation();
942
+ return;
943
+ }
903
944
  if (!approved) {
904
945
  yield pushEvent({
905
946
  type: "tool:approval:denied",
@@ -929,11 +970,20 @@ ${boundedMainMemory.trim()}`
929
970
  });
930
971
  }
931
972
  const batchStart = now();
973
+ if (isCancelled()) {
974
+ yield emitCancellation();
975
+ return;
976
+ }
932
977
  const batchResults =
933
978
  approvedCalls.length > 0
934
979
  ? await this.dispatcher.executeBatch(approvedCalls, toolContext)
935
980
  : [];
936
981
 
982
+ if (isCancelled()) {
983
+ yield emitCancellation();
984
+ return;
985
+ }
986
+
937
987
  for (const result of batchResults) {
938
988
  if (result.error) {
939
989
  yield pushEvent({
@@ -993,6 +1043,12 @@ ${boundedMainMemory.trim()}`
993
1043
  duration: now() - stepStart,
994
1044
  });
995
1045
  } catch (error) {
1046
+ if (isCancelled() || isAbortError(error)) {
1047
+ if (!cancellationEmitted) {
1048
+ yield emitCancellation();
1049
+ }
1050
+ return;
1051
+ }
996
1052
  yield pushEvent({
997
1053
  type: "run:error",
998
1054
  runId,
@@ -1044,6 +1100,14 @@ ${boundedMainMemory.trim()}`
1044
1100
  duration: 0,
1045
1101
  };
1046
1102
  }
1103
+ if (event.type === "run:cancelled") {
1104
+ finalResult = {
1105
+ status: "cancelled",
1106
+ steps: 0,
1107
+ tokens: { input: 0, output: 0, cached: 0 },
1108
+ duration: 0,
1109
+ };
1110
+ }
1047
1111
  }
1048
1112
 
1049
1113
  return {
@@ -45,6 +45,13 @@ export class ToolDispatcher {
45
45
  }
46
46
 
47
47
  async execute(call: ToolCall, context: ToolContext): Promise<ToolExecutionResult> {
48
+ if (context.abortSignal?.aborted) {
49
+ return {
50
+ callId: call.id,
51
+ tool: call.name,
52
+ error: "Tool execution cancelled",
53
+ };
54
+ }
48
55
  const definition = this.tools.get(call.name);
49
56
  if (!definition) {
50
57
  return {
@@ -56,12 +63,26 @@ export class ToolDispatcher {
56
63
 
57
64
  try {
58
65
  const output = await definition.handler(call.input, context);
66
+ if (context.abortSignal?.aborted) {
67
+ return {
68
+ callId: call.id,
69
+ tool: call.name,
70
+ error: "Tool execution cancelled",
71
+ };
72
+ }
59
73
  return {
60
74
  callId: call.id,
61
75
  tool: call.name,
62
76
  output,
63
77
  };
64
78
  } catch (error) {
79
+ if (context.abortSignal?.aborted) {
80
+ return {
81
+ callId: call.id,
82
+ tool: call.name,
83
+ error: "Tool execution cancelled",
84
+ };
85
+ }
65
86
  return {
66
87
  callId: call.id,
67
88
  tool: call.name,
@@ -74,6 +95,19 @@ export class ToolDispatcher {
74
95
  calls: ToolCall[],
75
96
  context: ToolContext,
76
97
  ): Promise<ToolExecutionResult[]> {
77
- return Promise.all(calls.map(async (call) => this.execute(call, context)));
98
+ const results: ToolExecutionResult[] = [];
99
+ for (const call of calls) {
100
+ if (context.abortSignal?.aborted) {
101
+ results.push({
102
+ callId: call.id,
103
+ tool: call.name,
104
+ error: "Tool execution cancelled",
105
+ });
106
+ continue;
107
+ }
108
+ // eslint-disable-next-line no-await-in-loop
109
+ results.push(await this.execute(call, context));
110
+ }
111
+ return results;
78
112
  }
79
113
  }