@vanillagreen/pi-claude-bridge 1.9.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -125
- package/bundle/connector-inventory.js +16 -3
- package/bundle/index.js +3743 -1810
- package/package.json +14 -23
- package/src/account-host.ts +112 -0
- package/src/account-router.ts +272 -0
- package/src/agents-md.ts +54 -10
- package/src/assistant-stream.ts +472 -66
- package/src/auth-presence.ts +6 -50
- package/src/bridge-commands.ts +86 -0
- package/src/bridge-state.ts +200 -15
- package/src/config.ts +170 -20
- package/src/connector-audit.ts +203 -0
- package/src/connector-cache.ts +148 -0
- package/src/connector-inventory.ts +66 -7
- package/src/connector-runtime.ts +158 -0
- package/src/connectors.ts +406 -19
- package/src/consume-query.ts +312 -0
- package/src/convert.ts +20 -10
- package/src/debug.ts +64 -6
- package/src/index.ts +901 -676
- package/src/models.ts +0 -7
- package/src/native-provider.ts +94 -0
- package/src/prompt-context.ts +5 -1
- package/src/query-options.ts +183 -0
- package/src/query-state.ts +490 -25
- package/src/query-teardown.ts +45 -0
- package/src/rate-limit.ts +48 -13
- package/src/request-lane.ts +36 -0
- package/src/sdk-query.ts +16 -0
- package/src/session-persistence.ts +370 -50
- package/src/tool-pairing-audit.ts +117 -0
- package/src/typebox-to-zod.ts +9 -3
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
// repairs them with synthetic "[no tool result recorded]" blocks.
|
|
3
3
|
// Kept pure so tests can exercise the exact audit without activating Pi.
|
|
4
4
|
|
|
5
|
+
export interface RecoveredToolResult {
|
|
6
|
+
id: string;
|
|
7
|
+
assistantIndex: number;
|
|
8
|
+
sourceUserIndex: number;
|
|
9
|
+
targetUserIndex: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface MissingToolResult {
|
|
6
13
|
id: string;
|
|
7
14
|
toolName: string;
|
|
@@ -27,6 +34,68 @@ function toolResultIds(content: unknown): Set<string> {
|
|
|
27
34
|
return ids;
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Pi can split a parallel Claude tool batch into several visible turns when a
|
|
39
|
+
* steer is drained after the first tool result. The first assistant message
|
|
40
|
+
* still contains every tool_use, while later sibling results sit behind small
|
|
41
|
+
* duplicate assistant/tool-result pairs. Anthropic history requires every
|
|
42
|
+
* result immediately after the original batch, so copy those already-recorded
|
|
43
|
+
* later results into that first user result message before generic repair adds
|
|
44
|
+
* a false "[no tool result recorded]" placeholder.
|
|
45
|
+
*
|
|
46
|
+
* The later pair stays intact because Pi also recorded its duplicate tool_use;
|
|
47
|
+
* copying is therefore required to keep both assistant messages valid.
|
|
48
|
+
*/
|
|
49
|
+
export function recoverLaterToolResults(
|
|
50
|
+
messages: Array<{ role?: string; content?: unknown }>,
|
|
51
|
+
): RecoveredToolResult[] {
|
|
52
|
+
const recovered: RecoveredToolResult[] = [];
|
|
53
|
+
for (let i = 0; i < messages.length; i++) {
|
|
54
|
+
const assistant = messages[i];
|
|
55
|
+
if (assistant?.role !== "assistant") continue;
|
|
56
|
+
const uses = toolUses(assistant.content);
|
|
57
|
+
if (uses.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
const target = messages[i + 1];
|
|
60
|
+
if (target?.role !== "user") continue;
|
|
61
|
+
const present = toolResultIds(target.content);
|
|
62
|
+
const missing = uses.filter((use) => !present.has(use.id));
|
|
63
|
+
if (missing.length === 0) continue;
|
|
64
|
+
|
|
65
|
+
for (const use of missing) {
|
|
66
|
+
let sourceBlock: Record<string, any> | undefined;
|
|
67
|
+
let sourceUserIndex = -1;
|
|
68
|
+
for (let j = i + 2; j < messages.length; j++) {
|
|
69
|
+
const candidate = messages[j];
|
|
70
|
+
if (candidate?.role !== "user") continue;
|
|
71
|
+
sourceBlock = contentBlocks(candidate.content).find(
|
|
72
|
+
(block) => block.type === "tool_result" && block.tool_use_id === use.id,
|
|
73
|
+
);
|
|
74
|
+
if (sourceBlock) {
|
|
75
|
+
sourceUserIndex = j;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!sourceBlock) continue;
|
|
80
|
+
|
|
81
|
+
const targetBlocks = Array.isArray(target.content)
|
|
82
|
+
? target.content as Array<Record<string, any>>
|
|
83
|
+
: typeof target.content === "string" && target.content
|
|
84
|
+
? [{ type: "text", text: target.content }]
|
|
85
|
+
: [];
|
|
86
|
+
// tool_result blocks must lead the user message; insert the recovered
|
|
87
|
+
// result after the existing tool_results, never after trailing text.
|
|
88
|
+
let insertAt = 0;
|
|
89
|
+
while (insertAt < targetBlocks.length && targetBlocks[insertAt]?.type === "tool_result") insertAt++;
|
|
90
|
+
targetBlocks.splice(insertAt, 0, { ...sourceBlock });
|
|
91
|
+
target.content = targetBlocks;
|
|
92
|
+
present.add(use.id);
|
|
93
|
+
recovered.push({ id: use.id, assistantIndex: i, sourceUserIndex, targetUserIndex: i + 1 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return recovered;
|
|
97
|
+
}
|
|
98
|
+
|
|
30
99
|
/**
|
|
31
100
|
* Anthropic history requires an assistant message containing tool_use blocks to
|
|
32
101
|
* be followed by a user message containing matching tool_result blocks. Return
|
|
@@ -52,6 +121,54 @@ export function findUnpairedToolUses(messages: Array<{ role?: string; content?:
|
|
|
52
121
|
return missing;
|
|
53
122
|
}
|
|
54
123
|
|
|
124
|
+
export const LOST_TOOL_RESULT_TEXT =
|
|
125
|
+
"Claude bridge: the result of this tool call was lost before the session was rebuilt "
|
|
126
|
+
+ "(the turn was interrupted). Treat the call as failed — it may or may not have executed. "
|
|
127
|
+
+ "Re-run the tool if its output is still needed.";
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Insert explicit, bridge-authored error results for every unpaired tool_use,
|
|
131
|
+
* IN PLACE, before cc-session-io's repairToolPairing runs.
|
|
132
|
+
*
|
|
133
|
+
* repairToolPairing backfills with a bare "[no tool result recorded]" — a
|
|
134
|
+
* placeholder the model reads as tool OUTPUT and keeps reasoning on (observed:
|
|
135
|
+
* two bash calls in the 2026-07-28 token test, silently treated as if they had
|
|
136
|
+
* returned). An is_error result that says what happened and what to do turns a
|
|
137
|
+
* silent correctness hazard into a recoverable failure.
|
|
138
|
+
*
|
|
139
|
+
* Results are prepended to the immediately following user message (tool_result
|
|
140
|
+
* blocks must lead a user message), or a new user message is inserted when none
|
|
141
|
+
* follows. `missing` must come from findUnpairedToolUses on the same array.
|
|
142
|
+
*/
|
|
143
|
+
export function insertLostToolResultPlaceholders(
|
|
144
|
+
messages: Array<{ role?: string; content?: unknown }>,
|
|
145
|
+
missing: MissingToolResult[],
|
|
146
|
+
): void {
|
|
147
|
+
const block = (id: string) => ({ type: "tool_result", tool_use_id: id, content: LOST_TOOL_RESULT_TEXT, is_error: true });
|
|
148
|
+
const byAssistant = new Map<number, MissingToolResult[]>();
|
|
149
|
+
for (const item of missing) {
|
|
150
|
+
const group = byAssistant.get(item.assistantIndex) ?? [];
|
|
151
|
+
group.push(item);
|
|
152
|
+
byAssistant.set(item.assistantIndex, group);
|
|
153
|
+
}
|
|
154
|
+
// Descending order so inserting a new user message never shifts an index a
|
|
155
|
+
// later (earlier-in-array) group still needs.
|
|
156
|
+
for (const assistantIndex of [...byAssistant.keys()].sort((a, b) => b - a)) {
|
|
157
|
+
const group = byAssistant.get(assistantIndex)!;
|
|
158
|
+
const blocks = group.map((item) => block(item.id));
|
|
159
|
+
const userIndex = group[0].userIndex;
|
|
160
|
+
if (userIndex != null && messages[userIndex]?.role === "user") {
|
|
161
|
+
const user = messages[userIndex] as { role: string; content: unknown };
|
|
162
|
+
const existing = typeof user.content === "string"
|
|
163
|
+
? (user.content ? [{ type: "text", text: user.content }] : [])
|
|
164
|
+
: Array.isArray(user.content) ? user.content : [];
|
|
165
|
+
user.content = [...blocks, ...existing];
|
|
166
|
+
} else {
|
|
167
|
+
messages.splice(assistantIndex + 1, 0, { role: "user", content: blocks });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
55
172
|
export function summarizeMissingToolNames(missing: MissingToolResult[]): Array<{ name: string; count: number }> {
|
|
56
173
|
const counts = new Map<string, number>();
|
|
57
174
|
for (const item of missing) counts.set(item.toolName, (counts.get(item.toolName) ?? 0) + 1);
|
package/src/typebox-to-zod.ts
CHANGED
|
@@ -28,9 +28,15 @@ export function jsonSchemaPropertyToZod(prop: Record<string, unknown>): z.ZodTyp
|
|
|
28
28
|
}
|
|
29
29
|
case "object": {
|
|
30
30
|
if (prop.properties && typeof prop.properties === "object" && !Array.isArray(prop.properties)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const obj = z.object(jsonSchemaToZodShape(prop));
|
|
32
|
+
// JSON Schema's default is PERMISSIVE (additionalProperties omitted
|
|
33
|
+
// means allowed); zod's default is to silently STRIP unknown keys.
|
|
34
|
+
// Stripping matters here: the MCP handler compares its validated
|
|
35
|
+
// input against the raw streamed tool_use input to claim a call id,
|
|
36
|
+
// so a silently dropped key made the two diverge and the claim fail
|
|
37
|
+
// (stranding the call — see claimToolCall). Only an explicit
|
|
38
|
+
// additionalProperties:false may reject/strip.
|
|
39
|
+
base = prop.additionalProperties === false ? obj.strict() : obj.passthrough();
|
|
34
40
|
} else {
|
|
35
41
|
base = z.record(z.string(), z.unknown());
|
|
36
42
|
}
|