@loopingai/core 0.8.2 → 0.8.3

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.
@@ -51,8 +51,9 @@ function byRound(branches) {
51
51
  * than carried, from the durable rows that are the record of what happened.
52
52
  *
53
53
  * Failed and skipped branches are included so the model can disclose them rather
54
- * than quietly answering as if the work had been done; their diagnostics are not
55
- * (see `delegateCallOutput`).
54
+ * than quietly answering as if the work had been done and, since a failed
55
+ * branch carries its reason in `output`, so it can tell a wall it should stop
56
+ * at from a hiccup worth retrying (see `delegateCallOutput`).
56
57
  */
57
58
  function delegationPair(taskId, round, replyText, branches) {
58
59
  const toolCallId = delegateToolCallId(taskId, round);
@@ -61,12 +61,29 @@ export declare function makeDelegateTool(types: SubtaskTypeRegistry, maxSubtasks
61
61
  */
62
62
  export declare function delegateToolCallId(taskId: string, round: number): string;
63
63
  /**
64
- * One branch's outcome, as the tool result carries it. `output` is null for any
65
- * branch that did not complete.
64
+ * One branch's outcome, as the tool result carries it. `output` carries the
65
+ * branch's report when it completed and its failure reason when it did not.
66
66
  *
67
- * There is no `error` field, and that is deliberate: internal diagnostics never
68
- * reach the model. It discloses *that* something failed, in user-safe words; the
69
- * durable row keeps the detail.
67
+ * **A failed branch says why, and that is a reversal worth explaining.** This
68
+ * used to be `null` for anything that did not complete, on the principle that
69
+ * internal diagnostics never reach the model. The principle was right about
70
+ * *diagnostics* and wrong about this field: what a facet writes into `error` is
71
+ * not a stack trace, it is a sentence addressed to the delegating model —
72
+ * "there is no checkout in this workspace yet… clone the repository before
73
+ * delegating", "every credential has reached its limit; send this request again
74
+ * after that". Withholding those left the parent with `status: "failed"` and
75
+ * nothing else, and a parent that cannot tell a transient failure from a
76
+ * permanent one retries. In the run that prompted this it retried twelve times
77
+ * over nine minutes, then apologised to the user for a wall it was never shown.
78
+ *
79
+ * What replaces the old rule is a constraint on the writer rather than a filter
80
+ * here: **an `error` is model-visible, so a facet must write it in words that
81
+ * are safe for one to read and act on.** Bounded by {@link MAX_OUTPUT_CHARS} on
82
+ * the way through, because a facet that ignores that is a context-window
83
+ * problem rather than a disclosure one.
84
+ *
85
+ * Still one field, not two. The model's question is "what came back from this
86
+ * branch", and `status` already says which kind of answer it is getting.
70
87
  *
71
88
  * A type alias, not an interface: this is serialized as the tool result's
72
89
  * `JSONValue`, and only aliases get the implicit index signature that satisfies.
@@ -93,5 +110,11 @@ export type DelegateSubtaskOutcome = {
93
110
  * and carries the durable `subtaskId`.
94
111
  */
95
112
  export declare function delegateCallInput(reply: string, branches: CompositionBranch[]): DecompositionProposal;
96
- /** Rebuild one round's call result from its durable rows, in stable ordinal order. */
113
+ /**
114
+ * Rebuild one round's call result from its durable rows, in stable ordinal order.
115
+ *
116
+ * A completed branch reports its parts; any other branch reports its `error`, or
117
+ * `null` when it has none to give — a cancelled branch usually does not, and
118
+ * inventing a sentence for it would be worse than the absence.
119
+ */
97
120
  export declare function delegateCallOutput(branches: CompositionBranch[]): DelegateSubtaskOutcome[];
@@ -68,6 +68,21 @@ export function makeDelegateTool(types, maxSubtasks) {
68
68
  export function delegateToolCallId(taskId, round) {
69
69
  return `task_${taskId}_round_${round}_delegate`;
70
70
  }
71
+ /**
72
+ * Ceiling on one branch's `output`, applied to both halves of it.
73
+ *
74
+ * A round's history holds every branch of every earlier round, so this is
75
+ * multiplied by the whole delegation history rather than paid once. Generous
76
+ * enough for a report a subagent meant to be read, far short of a build log a
77
+ * failing one dumped into `error`.
78
+ */
79
+ const MAX_OUTPUT_CHARS = 8_000;
80
+ function bounded(text) {
81
+ if (text.length <= MAX_OUTPUT_CHARS)
82
+ return text;
83
+ const suffix = "\n…[truncated]";
84
+ return `${text.slice(0, MAX_OUTPUT_CHARS - suffix.length)}${suffix}`;
85
+ }
71
86
  /**
72
87
  * Rebuild one round's call input from its durable rows, in stable ordinal order.
73
88
  * Typed as {@link DecompositionProposal} — the same type the model's own calls
@@ -95,14 +110,22 @@ export function delegateCallInput(reply, branches) {
95
110
  }))
96
111
  };
97
112
  }
98
- /** Rebuild one round's call result from its durable rows, in stable ordinal order. */
113
+ /**
114
+ * Rebuild one round's call result from its durable rows, in stable ordinal order.
115
+ *
116
+ * A completed branch reports its parts; any other branch reports its `error`, or
117
+ * `null` when it has none to give — a cancelled branch usually does not, and
118
+ * inventing a sentence for it would be worse than the absence.
119
+ */
99
120
  export function delegateCallOutput(branches) {
100
121
  return branches.map((branch) => ({
101
122
  subtaskId: branch.subtaskId,
102
123
  type: branch.type,
103
124
  status: branch.status,
104
125
  output: branch.status === "completed"
105
- ? (branch.resultParts ?? []).map((part) => part.text).join("\n")
106
- : null
126
+ ? bounded((branch.resultParts ?? []).map((part) => part.text).join("\n"))
127
+ : branch.error
128
+ ? bounded(branch.error)
129
+ : null
107
130
  }));
108
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopingai/core",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Shared, mandatory foundation for Looping agents on Cloudflare Workers: zero-trust A2A, durable task lifecycle, delegation and subagent runtime, test harness.",
5
5
  "keywords": [
6
6
  "a2a",