pi2dsh 0.12.1 → 0.12.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.
- package/README.md +37 -7
- package/README.zh.md +32 -6
- package/dist/{analyzer-CsrBeS8r.mjs → analyzer-2Nx5XYOA.mjs} +3 -3
- package/dist/{analyzer-CsrBeS8r.mjs.map → analyzer-2Nx5XYOA.mjs.map} +1 -1
- package/dist/cli.mjs +7 -44
- package/dist/cli.mjs.map +1 -1
- package/dist/client.js +139 -0
- package/dist/compat/pi-coding-agent.d.mts +82 -14
- package/dist/compat/pi-coding-agent.d.mts.map +1 -1
- package/dist/compat/pi-coding-agent.mjs +1 -1
- package/dist/credentials-oauth.mjs +1 -1
- package/dist/host.d.mts +1 -11
- package/dist/host.d.mts.map +1 -1
- package/dist/host.mjs +3 -98
- package/dist/host.mjs.map +1 -1
- package/dist/index.d.mts +18 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -14
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-config-Cjae0vot.mjs +766 -0
- package/dist/mcp-config-Cjae0vot.mjs.map +1 -0
- package/dist/{pi-coding-agent-Ctw-BhoY.mjs → pi-coding-agent-CYnmeJEx.mjs} +0 -0
- package/dist/pi-coding-agent-CYnmeJEx.mjs.map +1 -0
- package/dist/{runtime-Xy8yU8wO.mjs → runtime-B_Ig28og.mjs} +1122 -223
- package/dist/runtime-B_Ig28og.mjs.map +1 -0
- package/dist/runtime.d.mts +33 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -1
- package/package.json +23 -9
- package/dist/generator-CSOv6wYT.mjs +0 -298
- package/dist/generator-CSOv6wYT.mjs.map +0 -1
- package/dist/mcp-config-Cn5gWemZ.mjs +0 -707
- package/dist/mcp-config-Cn5gWemZ.mjs.map +0 -1
- package/dist/pi-coding-agent-Ctw-BhoY.mjs.map +0 -1
- package/dist/runtime-Xy8yU8wO.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import { F as AssistantMessageEventStream, a as builtinProviders, h as getSupportedThinkingLevels, i as __setPiAiLlmBridge } from "./pi-ai-Dyg4zyLZ.mjs";
|
|
3
|
-
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-
|
|
2
|
+
import { F as AssistantMessageEventStream, I as isContextOverflow, a as builtinProviders, h as getSupportedThinkingLevels, i as __setPiAiLlmBridge } from "./pi-ai-Dyg4zyLZ.mjs";
|
|
3
|
+
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-CYnmeJEx.mjs";
|
|
4
4
|
import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
|
|
5
5
|
import { a as resolvePiProviderAuth, n as loginPiProvider, r as providerSupportsOAuth, t as FileCredentialStore } from "./oauth-bridge-CfyImWx4.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -12,10 +12,18 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
14
|
import { createJiti } from "jiti";
|
|
15
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
15
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
16
16
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { foldSurface } from "@deepseek-ai/dsh-session";
|
|
19
|
+
import { Compile } from "typebox/compile";
|
|
20
|
+
import { Value } from "typebox/value";
|
|
18
21
|
//#region src/session-bridge.ts
|
|
22
|
+
/** The durable seq behind a projected entry id (`dsh-<seq>`), when it has one. */
|
|
23
|
+
function entrySeq(id) {
|
|
24
|
+
const match = /^dsh-(\d+)$/.exec(id);
|
|
25
|
+
return match === null ? void 0 : Number(match[1]);
|
|
26
|
+
}
|
|
19
27
|
function sidecarDir() {
|
|
20
28
|
return join(getAgentDir(), "session-entries");
|
|
21
29
|
}
|
|
@@ -143,6 +151,20 @@ var PiSessionBridge = class {
|
|
|
143
151
|
* shape. DSH history is linear, so the projection is a single-branch tree:
|
|
144
152
|
* every entry's parent is its predecessor.
|
|
145
153
|
*/
|
|
154
|
+
/**
|
|
155
|
+
* The seqs still on the model-visible surface, via DSH's own canonical fold.
|
|
156
|
+
* @param session - the session to project.
|
|
157
|
+
* @returns the visible seqs, or undefined when the fold cannot run (a
|
|
158
|
+
* projection built from a partial event list, e.g. in a test double) — in
|
|
159
|
+
* which case nothing is filtered out rather than everything.
|
|
160
|
+
*/
|
|
161
|
+
visibleSeqs(session) {
|
|
162
|
+
try {
|
|
163
|
+
return new Set(foldSurface(sessionEvents(session)).nodes);
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
146
168
|
projectEntries(session) {
|
|
147
169
|
this.load(session.id);
|
|
148
170
|
const merged = [];
|
|
@@ -270,7 +292,14 @@ var PiSessionBridge = class {
|
|
|
270
292
|
const index = entries.findIndex((entry) => entry.id === fromId);
|
|
271
293
|
return index === -1 ? [] : entries.slice(0, index + 1);
|
|
272
294
|
},
|
|
273
|
-
buildContextEntries: () =>
|
|
295
|
+
buildContextEntries: () => {
|
|
296
|
+
const visible = this.visibleSeqs(session);
|
|
297
|
+
return entriesOf().filter((entry) => {
|
|
298
|
+
if (entry.type !== "message" && entry.type !== "compaction" && entry.type !== "branch_summary" && entry.type !== "custom_message") return false;
|
|
299
|
+
const seq = entrySeq(entry.id);
|
|
300
|
+
return seq === void 0 || visible === void 0 || visible.has(seq);
|
|
301
|
+
});
|
|
302
|
+
},
|
|
274
303
|
getHeader: () => ({
|
|
275
304
|
type: "session",
|
|
276
305
|
version: 3,
|
|
@@ -319,6 +348,7 @@ var PiBridgedAgentSession = class {
|
|
|
319
348
|
agent = {};
|
|
320
349
|
messages = [];
|
|
321
350
|
#seed = [];
|
|
351
|
+
#carried = /* @__PURE__ */ new WeakSet();
|
|
322
352
|
#host;
|
|
323
353
|
#handle;
|
|
324
354
|
#session;
|
|
@@ -332,6 +362,8 @@ var PiBridgedAgentSession = class {
|
|
|
332
362
|
#pendingToolCalls = /* @__PURE__ */ new Set();
|
|
333
363
|
#sessionName = "";
|
|
334
364
|
#turns = 0;
|
|
365
|
+
/** Keeps message projections (which await attachment reads) in log order. */
|
|
366
|
+
#messageProjection = Promise.resolve();
|
|
335
367
|
#aborted = false;
|
|
336
368
|
constructor(host, handle, tools) {
|
|
337
369
|
this.#host = host;
|
|
@@ -402,7 +434,19 @@ var PiBridgedAgentSession = class {
|
|
|
402
434
|
const identity = (message) => JSON.stringify([message.role, message.content]);
|
|
403
435
|
const known = new Set(this.messages.map(identity));
|
|
404
436
|
this.#seed = next.filter((message) => !known.has(identity(message)));
|
|
405
|
-
for (const message of this.#seed)
|
|
437
|
+
for (const message of this.#seed) {
|
|
438
|
+
this.#carried.add(message);
|
|
439
|
+
this.messages.push(message);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Whether one transcript entry is carried context rather than part of the
|
|
444
|
+
* exchange in this child session.
|
|
445
|
+
* @param message - an entry of {@link messages}.
|
|
446
|
+
* @returns true for a seeded transcript entry or a host runtime snapshot.
|
|
447
|
+
*/
|
|
448
|
+
isCarriedContext(message) {
|
|
449
|
+
return this.#carried.has(message);
|
|
406
450
|
}
|
|
407
451
|
#project(event) {
|
|
408
452
|
const type = event.type;
|
|
@@ -464,8 +508,10 @@ var PiBridgedAgentSession = class {
|
|
|
464
508
|
}
|
|
465
509
|
});
|
|
466
510
|
}
|
|
467
|
-
|
|
468
|
-
|
|
511
|
+
this.#messageProjection = this.#messageProjection.then(async () => {
|
|
512
|
+
const message = await this.#host.messageFromSessionEvent(event);
|
|
513
|
+
if (message === void 0) return;
|
|
514
|
+
if ((event.data?.source)?.form === "snapshot") this.#carried.add(message);
|
|
469
515
|
this.messages.push(message);
|
|
470
516
|
emit({
|
|
471
517
|
type: "message_start",
|
|
@@ -475,19 +521,21 @@ var PiBridgedAgentSession = class {
|
|
|
475
521
|
type: "message_end",
|
|
476
522
|
message
|
|
477
523
|
});
|
|
478
|
-
}
|
|
524
|
+
});
|
|
479
525
|
if (type === "turn/end") {
|
|
480
526
|
this.#turns += 1;
|
|
481
527
|
this.#streaming = false;
|
|
482
528
|
this.#pendingToolCalls.clear();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
529
|
+
this.#messageProjection = this.#messageProjection.then(() => {
|
|
530
|
+
emit({
|
|
531
|
+
type: "turn_end",
|
|
532
|
+
turnIndex: Number(event.data.turn ?? 1) - 1,
|
|
533
|
+
message: {
|
|
534
|
+
role: "assistant",
|
|
535
|
+
content: []
|
|
536
|
+
},
|
|
537
|
+
toolResults: []
|
|
538
|
+
});
|
|
491
539
|
});
|
|
492
540
|
}
|
|
493
541
|
}
|
|
@@ -507,7 +555,7 @@ var PiBridgedAgentSession = class {
|
|
|
507
555
|
if (rendered.length > 0) {
|
|
508
556
|
const seedBlocks = await this.#host.piContentToDsh([{
|
|
509
557
|
type: "text",
|
|
510
|
-
text:
|
|
558
|
+
text: `<${SEED_CARRIER_TAG}>\n${rendered.join("\n\n")}\n</${SEED_CARRIER_TAG}>`
|
|
511
559
|
}]);
|
|
512
560
|
this.#host.deliver(this.#handle.agent, createUserMessage({
|
|
513
561
|
content: seedBlocks,
|
|
@@ -541,6 +589,7 @@ var PiBridgedAgentSession = class {
|
|
|
541
589
|
}
|
|
542
590
|
}), "followup");
|
|
543
591
|
await completed;
|
|
592
|
+
await this.#messageProjection;
|
|
544
593
|
}
|
|
545
594
|
steer(text) {
|
|
546
595
|
this.#host.piContentToDsh([{
|
|
@@ -604,6 +653,15 @@ var PiBridgedAgentSession = class {
|
|
|
604
653
|
* @param requested - a label the calling package supplied, if any.
|
|
605
654
|
* @param packageName - the Pi package the call came from.
|
|
606
655
|
*/
|
|
656
|
+
/**
|
|
657
|
+
* Envelope the bridge wraps a seeded transcript in before handing it to the
|
|
658
|
+
* child as one durable message.
|
|
659
|
+
*
|
|
660
|
+
* Exported because it is a CONTRACT, not a formatting detail: a reader of the
|
|
661
|
+
* child session (the side panel) has to recognise the carrier to tell carried
|
|
662
|
+
* context apart from the exchange, and matching on a copied string would drift.
|
|
663
|
+
*/
|
|
664
|
+
const SEED_CARRIER_TAG = "prior-conversation";
|
|
607
665
|
function childLabel(requested, packageName) {
|
|
608
666
|
if (typeof requested === "string" && requested.trim().length > 0) return requested.trim().slice(0, 80);
|
|
609
667
|
const owner = packageName?.trim();
|
|
@@ -663,6 +721,283 @@ async function createBridgedAgentSession(host, options) {
|
|
|
663
721
|
return { session: new PiBridgedAgentSession(host, handle, tools) };
|
|
664
722
|
}
|
|
665
723
|
//#endregion
|
|
724
|
+
//#region src/side-panel.ts
|
|
725
|
+
/** Flatten one Pi message's content to the text a panel row shows. */
|
|
726
|
+
function messageText(message) {
|
|
727
|
+
const content = message.content;
|
|
728
|
+
if (typeof content === "string") return content;
|
|
729
|
+
if (!Array.isArray(content)) return "";
|
|
730
|
+
const parts = [];
|
|
731
|
+
for (const block of content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
732
|
+
else if (block.type === "toolCall" && typeof block.name === "string") parts.push(`[tool: ${block.name}]`);
|
|
733
|
+
return parts.join("\n");
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Live side threads, keyed by the parent session they belong to.
|
|
737
|
+
*
|
|
738
|
+
* Host-level (one per engine instance, like the provider directory): the panel
|
|
739
|
+
* is one surface, however many Pi packages contribute threads to it.
|
|
740
|
+
*/
|
|
741
|
+
var SidePanelRegistry = class {
|
|
742
|
+
#byParent = /* @__PURE__ */ new Map();
|
|
743
|
+
/**
|
|
744
|
+
* Track one child session under its parent.
|
|
745
|
+
* @param parentSessionId - the session the panel floats over.
|
|
746
|
+
* @param thread - the child's identity and live session object.
|
|
747
|
+
* @returns a disposer that removes the thread from the panel.
|
|
748
|
+
*/
|
|
749
|
+
track(parentSessionId, thread) {
|
|
750
|
+
const threads = this.#byParent.get(parentSessionId) ?? /* @__PURE__ */ new Map();
|
|
751
|
+
threads.set(thread.id, thread);
|
|
752
|
+
this.#byParent.set(parentSessionId, threads);
|
|
753
|
+
return () => {
|
|
754
|
+
const live = this.#byParent.get(parentSessionId);
|
|
755
|
+
if (live === void 0) return;
|
|
756
|
+
live.delete(thread.id);
|
|
757
|
+
if (live.size === 0) this.#byParent.delete(parentSessionId);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* The panel's view of one parent session.
|
|
762
|
+
* @param parentSessionId - session the browser is currently showing.
|
|
763
|
+
* @returns every side thread opened under it, oldest first.
|
|
764
|
+
*/
|
|
765
|
+
snapshot(parentSessionId) {
|
|
766
|
+
const threads = this.#byParent.get(parentSessionId);
|
|
767
|
+
if (threads === void 0) return [];
|
|
768
|
+
return [...threads.values()].map((thread) => ({
|
|
769
|
+
id: thread.id,
|
|
770
|
+
label: thread.label,
|
|
771
|
+
...thread.package === void 0 ? {} : { package: thread.package },
|
|
772
|
+
running: thread.session.isStreaming === true,
|
|
773
|
+
messages: thread.session.messages.filter((message) => !thread.session.isCarriedContext(message)).map((message) => ({
|
|
774
|
+
role: String(message.role ?? ""),
|
|
775
|
+
text: messageText(message)
|
|
776
|
+
})).filter((entry) => !entry.text.startsWith(`<${SEED_CARRIER_TAG}>`)).filter((entry) => entry.text.length > 0)
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* Serve the panel's own read route on the host's web server.
|
|
782
|
+
*
|
|
783
|
+
* One prefix, one GET: the client half polls it for the session it is showing.
|
|
784
|
+
* A composition with no web server (the CLI profile) simply has no panel, which
|
|
785
|
+
* is the correct outcome rather than an error — the side conversation itself
|
|
786
|
+
* still runs, and the host's subagent catalog still lists it.
|
|
787
|
+
* @param ctx - the mounting context.
|
|
788
|
+
* @param registry - live thread registry to read from.
|
|
789
|
+
* @returns whether the route was registered.
|
|
790
|
+
*/
|
|
791
|
+
function registerSidePanelRoute(ctx, registry) {
|
|
792
|
+
const web = ctx.get("webServer");
|
|
793
|
+
if (web === void 0 || typeof web.register !== "function") return false;
|
|
794
|
+
ctx.effect(() => web.register({
|
|
795
|
+
kind: "prefix",
|
|
796
|
+
path: "/pi2dsh",
|
|
797
|
+
handler: async (req, res) => {
|
|
798
|
+
const response = res;
|
|
799
|
+
const method = String(req.method ?? "GET");
|
|
800
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
801
|
+
response.writeHead(405);
|
|
802
|
+
response.end();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const url = new URL(String(req.url ?? "/"), "http://pi2dsh.invalid");
|
|
806
|
+
if (url.pathname !== "/pi2dsh/side-panel") {
|
|
807
|
+
response.writeHead(404);
|
|
808
|
+
response.end();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const session = url.searchParams.get("session") ?? "";
|
|
812
|
+
const body = JSON.stringify({ threads: session === "" ? [] : registry.snapshot(session) });
|
|
813
|
+
response.writeHead(200, {
|
|
814
|
+
"content-type": "application/json; charset=utf-8",
|
|
815
|
+
"cache-control": "no-store"
|
|
816
|
+
});
|
|
817
|
+
response.end(method === "HEAD" ? void 0 : body);
|
|
818
|
+
}
|
|
819
|
+
}), "pi2dsh: side-panel route");
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/compat/vendor/pi-tool-validation.ts
|
|
824
|
+
const validatorCache = /* @__PURE__ */ new WeakMap();
|
|
825
|
+
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
|
826
|
+
function getSchemaTypes(schema) {
|
|
827
|
+
if (typeof schema.type === "string") return [schema.type];
|
|
828
|
+
if (Array.isArray(schema.type)) return schema.type.filter((type) => typeof type === "string");
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
function matchesJsonType(value, type) {
|
|
832
|
+
switch (type) {
|
|
833
|
+
case "number": return typeof value === "number";
|
|
834
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
835
|
+
case "boolean": return typeof value === "boolean";
|
|
836
|
+
case "string": return typeof value === "string";
|
|
837
|
+
case "null": return value === null;
|
|
838
|
+
case "array": return Array.isArray(value);
|
|
839
|
+
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
840
|
+
default: return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function getSubSchemaValidator(schema) {
|
|
844
|
+
try {
|
|
845
|
+
return getValidator(schema);
|
|
846
|
+
} catch {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function coercePrimitiveByType(value, type) {
|
|
851
|
+
switch (type) {
|
|
852
|
+
case "number":
|
|
853
|
+
if (value === null) return 0;
|
|
854
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
855
|
+
const parsed = Number(value);
|
|
856
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
857
|
+
}
|
|
858
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
859
|
+
return value;
|
|
860
|
+
case "integer":
|
|
861
|
+
if (value === null) return 0;
|
|
862
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
863
|
+
const parsed = Number(value);
|
|
864
|
+
if (Number.isInteger(parsed)) return parsed;
|
|
865
|
+
}
|
|
866
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
867
|
+
return value;
|
|
868
|
+
case "boolean":
|
|
869
|
+
if (value === null) return false;
|
|
870
|
+
if (typeof value === "string") {
|
|
871
|
+
if (value === "true") return true;
|
|
872
|
+
if (value === "false") return false;
|
|
873
|
+
}
|
|
874
|
+
if (typeof value === "number") {
|
|
875
|
+
if (value === 1) return true;
|
|
876
|
+
if (value === 0) return false;
|
|
877
|
+
}
|
|
878
|
+
return value;
|
|
879
|
+
case "string":
|
|
880
|
+
if (value === null) return "";
|
|
881
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
882
|
+
return value;
|
|
883
|
+
case "null":
|
|
884
|
+
if (value === "" || value === 0 || value === false) return null;
|
|
885
|
+
return value;
|
|
886
|
+
default: return value;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function applySchemaObjectCoercion(value, schema) {
|
|
890
|
+
const properties = schema.properties;
|
|
891
|
+
const definedKeys = new Set(properties ? Object.keys(properties) : []);
|
|
892
|
+
if (properties) for (const [key, propertySchema] of Object.entries(properties)) {
|
|
893
|
+
if (!(key in value)) continue;
|
|
894
|
+
value[key] = coerceWithJsonSchema(value[key], propertySchema);
|
|
895
|
+
}
|
|
896
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") for (const [key, propertyValue] of Object.entries(value)) {
|
|
897
|
+
if (definedKeys.has(key)) continue;
|
|
898
|
+
value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function applySchemaArrayCoercion(value, schema) {
|
|
902
|
+
if (Array.isArray(schema.items)) {
|
|
903
|
+
for (let index = 0; index < value.length; index++) {
|
|
904
|
+
const itemSchema = schema.items[index];
|
|
905
|
+
if (!itemSchema) continue;
|
|
906
|
+
value[index] = coerceWithJsonSchema(value[index], itemSchema);
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (schema.items && typeof schema.items === "object") for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
|
|
911
|
+
}
|
|
912
|
+
function coerceWithUnionSchema(value, schemas) {
|
|
913
|
+
for (const schema of schemas) if (getSubSchemaValidator(schema)?.Check(value)) return value;
|
|
914
|
+
for (const schema of schemas) {
|
|
915
|
+
const coerced = coerceWithJsonSchema(structuredClone(value), schema);
|
|
916
|
+
if (getSubSchemaValidator(schema)?.Check(coerced)) return coerced;
|
|
917
|
+
}
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
920
|
+
function coerceWithJsonSchema(value, schema) {
|
|
921
|
+
let nextValue = value;
|
|
922
|
+
if (Array.isArray(schema.allOf)) for (const nested of schema.allOf) nextValue = coerceWithJsonSchema(nextValue, nested);
|
|
923
|
+
if (Array.isArray(schema.anyOf)) nextValue = coerceWithUnionSchema(nextValue, schema.anyOf);
|
|
924
|
+
if (Array.isArray(schema.oneOf)) nextValue = coerceWithUnionSchema(nextValue, schema.oneOf);
|
|
925
|
+
const schemaTypes = getSchemaTypes(schema);
|
|
926
|
+
const matchesUnionMember = schemaTypes.length > 1 && schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));
|
|
927
|
+
if (schemaTypes.length > 0 && !matchesUnionMember) for (const schemaType of schemaTypes) {
|
|
928
|
+
const candidate = coercePrimitiveByType(nextValue, schemaType);
|
|
929
|
+
if (candidate !== nextValue) {
|
|
930
|
+
nextValue = candidate;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (schemaTypes.includes("object") && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
|
|
935
|
+
if (schemaTypes.includes("array") && Array.isArray(nextValue)) applySchemaArrayCoercion(nextValue, schema);
|
|
936
|
+
return nextValue;
|
|
937
|
+
}
|
|
938
|
+
function normalizeOptionalNulls(value, schema) {
|
|
939
|
+
if (Array.isArray(value)) {
|
|
940
|
+
if (Array.isArray(schema.items)) for (let index = 0; index < value.length; index++) {
|
|
941
|
+
const itemSchema = schema.items[index];
|
|
942
|
+
if (itemSchema) normalizeOptionalNulls(value[index], itemSchema);
|
|
943
|
+
}
|
|
944
|
+
else if (schema.items) for (const item of value) normalizeOptionalNulls(item, schema.items);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (typeof value !== "object" || value === null || !schema.properties) return;
|
|
948
|
+
const object = value;
|
|
949
|
+
const required = new Set(schema.required ?? []);
|
|
950
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
951
|
+
if (!(key in object)) continue;
|
|
952
|
+
if (object[key] === null && !required.has(key) && typeof propertySchema.$ref !== "string" && getSubSchemaValidator(propertySchema)?.Check(null) === false) delete object[key];
|
|
953
|
+
else normalizeOptionalNulls(object[key], propertySchema);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function getValidator(schema) {
|
|
957
|
+
const key = schema;
|
|
958
|
+
const cached = validatorCache.get(key);
|
|
959
|
+
if (cached) return cached;
|
|
960
|
+
const validator = Compile(schema);
|
|
961
|
+
validatorCache.set(key, validator);
|
|
962
|
+
return validator;
|
|
963
|
+
}
|
|
964
|
+
function formatValidationPath(error) {
|
|
965
|
+
if (error.keyword === "required") {
|
|
966
|
+
const requiredProperty = error.params.requiredProperties?.[0];
|
|
967
|
+
if (requiredProperty) {
|
|
968
|
+
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
969
|
+
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return error.instancePath.replace(/^\//, "").replace(/\//g, ".") || "root";
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Validates tool call arguments against the tool's TypeBox schema
|
|
976
|
+
* @param tool The tool definition with TypeBox schema
|
|
977
|
+
* @param toolCall The tool call from the LLM
|
|
978
|
+
* @returns The validated (and potentially coerced) arguments
|
|
979
|
+
* @throws Error with formatted message if validation fails
|
|
980
|
+
*/
|
|
981
|
+
function validateToolArguments(tool, toolCall) {
|
|
982
|
+
const args = structuredClone(toolCall.arguments);
|
|
983
|
+
normalizeOptionalNulls(args, tool.parameters);
|
|
984
|
+
Value.Convert(tool.parameters, args);
|
|
985
|
+
const validator = getValidator(tool.parameters);
|
|
986
|
+
if (!Object.getOwnPropertySymbols(tool.parameters).includes(TYPEBOX_KIND)) {
|
|
987
|
+
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
|
988
|
+
if (coerced !== args) {
|
|
989
|
+
if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) {
|
|
990
|
+
for (const key of Object.keys(args)) delete args[key];
|
|
991
|
+
Object.assign(args, coerced);
|
|
992
|
+
} else return validator.Check(coerced) ? coerced : args;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (validator.Check(args)) return args;
|
|
996
|
+
const errors = validator.Errors(args).map((error) => ` - ${formatValidationPath(error)}: ${error.message}`).join("\n") || "Unknown validation error";
|
|
997
|
+
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
|
|
998
|
+
throw new Error(errorMessage);
|
|
999
|
+
}
|
|
1000
|
+
//#endregion
|
|
666
1001
|
//#region src/model-bridge.ts
|
|
667
1002
|
function llmOf(ctx) {
|
|
668
1003
|
try {
|
|
@@ -736,14 +1071,30 @@ var ModelCatalog = class {
|
|
|
736
1071
|
provider: provider.id
|
|
737
1072
|
}));
|
|
738
1073
|
} catch {}
|
|
1074
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1075
|
+
await Promise.all(next.map(async (model) => {
|
|
1076
|
+
const exact = await this.#resolveExact(model.provider, model.id);
|
|
1077
|
+
if (exact === void 0) return;
|
|
1078
|
+
if (exact.contextWindow !== void 0) model.contextWindow = exact.contextWindow;
|
|
1079
|
+
if (exact.maxTokens !== void 0) model.maxTokens = exact.maxTokens;
|
|
1080
|
+
model.reasoning = exact.reasoning;
|
|
1081
|
+
resolved.set(`${model.provider}${model.id}`, model);
|
|
1082
|
+
}));
|
|
739
1083
|
this.#models = next;
|
|
1084
|
+
this.#resolved = resolved;
|
|
740
1085
|
}
|
|
741
1086
|
/** Exact-route metadata (context window, reasoning efforts), cached per route. */
|
|
742
1087
|
async resolve(provider, modelId) {
|
|
743
|
-
if (this.#llm === void 0) return void 0;
|
|
744
1088
|
const key = `${provider}${modelId}`;
|
|
745
1089
|
const cached = this.#resolved.get(key);
|
|
746
1090
|
if (cached !== void 0) return cached;
|
|
1091
|
+
const model = await this.#resolveExact(provider, modelId);
|
|
1092
|
+
if (model !== void 0) this.#resolved.set(key, model);
|
|
1093
|
+
return model;
|
|
1094
|
+
}
|
|
1095
|
+
/** One exact resolve, uncached: the two callers own their own caching. */
|
|
1096
|
+
async #resolveExact(provider, modelId) {
|
|
1097
|
+
if (this.#llm === void 0) return void 0;
|
|
747
1098
|
try {
|
|
748
1099
|
const info = await this.#llm.resolveModelInfo(provider, modelId);
|
|
749
1100
|
const model = piModelFromInfo(info);
|
|
@@ -753,7 +1104,6 @@ var ModelCatalog = class {
|
|
|
753
1104
|
if (typeof defaultMaxTokens === "number") model.maxTokens = defaultMaxTokens;
|
|
754
1105
|
const reasoning = info.reasoning;
|
|
755
1106
|
model.reasoning = Array.isArray(reasoning?.efforts) && reasoning.efforts.length > 0;
|
|
756
|
-
this.#resolved.set(key, model);
|
|
757
1107
|
return model;
|
|
758
1108
|
} catch {
|
|
759
1109
|
return;
|
|
@@ -1083,26 +1433,34 @@ function normalizeToolSchemas(tools) {
|
|
|
1083
1433
|
}));
|
|
1084
1434
|
}
|
|
1085
1435
|
/** DSH GenerateOptions parts → the Pi request context a provider.stream expects. */
|
|
1086
|
-
function dshRequestToPiContext(options) {
|
|
1436
|
+
async function dshRequestToPiContext(options, attachments) {
|
|
1087
1437
|
const piMessages = [];
|
|
1438
|
+
const toolNamesById = /* @__PURE__ */ new Map();
|
|
1439
|
+
for (const message of Array.isArray(options.messages) ? options.messages : []) {
|
|
1440
|
+
if (message.role !== "assistant") continue;
|
|
1441
|
+
for (const block of Array.isArray(message.content) ? message.content : []) if (block.type === "tool-call" && typeof block.id === "string" && typeof block.name === "string") toolNamesById.set(block.id, block.name);
|
|
1442
|
+
}
|
|
1088
1443
|
for (const message of Array.isArray(options.messages) ? options.messages : []) {
|
|
1089
1444
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
1090
1445
|
const source = message.source;
|
|
1091
1446
|
if (message.role === "user" && source?.kind === "tool") {
|
|
1092
1447
|
const result = content[0];
|
|
1448
|
+
const toolCallId = String(result?.toolCallId ?? "");
|
|
1449
|
+
const toolName = toolNamesById.get(toolCallId);
|
|
1093
1450
|
piMessages.push({
|
|
1094
1451
|
role: "toolResult",
|
|
1095
|
-
toolCallId
|
|
1096
|
-
|
|
1452
|
+
toolCallId,
|
|
1453
|
+
...toolName === void 0 ? {} : { toolName },
|
|
1454
|
+
content: await piContentFromDshBlocks(Array.isArray(result?.content) ? result.content : [], attachments),
|
|
1097
1455
|
isError: result?.isError === true
|
|
1098
1456
|
});
|
|
1099
1457
|
} else if (message.role === "user") piMessages.push({
|
|
1100
1458
|
role: "user",
|
|
1101
|
-
content: piContentFromDshBlocks(content)
|
|
1459
|
+
content: await piContentFromDshBlocks(content, attachments)
|
|
1102
1460
|
});
|
|
1103
1461
|
else if (message.role === "assistant") piMessages.push({
|
|
1104
1462
|
role: "assistant",
|
|
1105
|
-
content: piContentFromDshBlocks(content)
|
|
1463
|
+
content: await piContentFromDshBlocks(content, attachments)
|
|
1106
1464
|
});
|
|
1107
1465
|
}
|
|
1108
1466
|
return {
|
|
@@ -1111,7 +1469,12 @@ function dshRequestToPiContext(options) {
|
|
|
1111
1469
|
...Array.isArray(options.tools) ? { tools: options.tools } : {}
|
|
1112
1470
|
};
|
|
1113
1471
|
}
|
|
1114
|
-
|
|
1472
|
+
/** Whether any block in this content — including inside a tool result — is an image. */
|
|
1473
|
+
function blocksContainImage(blocks) {
|
|
1474
|
+
if (!Array.isArray(blocks)) return false;
|
|
1475
|
+
return blocks.some((block) => block?.type === "image" || block?.type === "tool-result" && blocksContainImage(block.content));
|
|
1476
|
+
}
|
|
1477
|
+
async function piContentFromDshBlocks(blocks, attachments) {
|
|
1115
1478
|
const out = [];
|
|
1116
1479
|
for (const block of blocks) if (block.type === "text") out.push({
|
|
1117
1480
|
type: "text",
|
|
@@ -1134,15 +1497,187 @@ function piContentFromDshBlocks(blocks) {
|
|
|
1134
1497
|
name: String(block.name ?? ""),
|
|
1135
1498
|
arguments: parsed
|
|
1136
1499
|
});
|
|
1500
|
+
} else if (block.type === "image") {
|
|
1501
|
+
if (attachments === void 0) throw new LlmError("pi2dsh: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1502
|
+
const stored = await attachments.readImage(block.attachment);
|
|
1503
|
+
out.push({
|
|
1504
|
+
type: "image",
|
|
1505
|
+
data: Buffer.from(stored.data).toString("base64"),
|
|
1506
|
+
mimeType: String(stored.ref?.mediaType ?? block.attachment?.mediaType ?? "image/png")
|
|
1507
|
+
});
|
|
1137
1508
|
}
|
|
1138
1509
|
return out;
|
|
1139
1510
|
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Classify a failure into the code DSH keys retries and error surfaces off.
|
|
1513
|
+
*
|
|
1514
|
+
* Mirrored from `classifyPiAiError` in DSH's own pi-ai adapter
|
|
1515
|
+
* (`packages/llm/llm-pi-ai/src/stream.ts`). That symbol is private to a
|
|
1516
|
+
* package whose published tarball ships only `lib/`, so it cannot be imported
|
|
1517
|
+
* — but it must not be diverged from either. `RATE_LIMIT`, `SERVER`,
|
|
1518
|
+
* `TIMEOUT` and `TRANSPORT` are four of the five codes in DSH's default
|
|
1519
|
+
* retryable set, so a code invented here would make every rate limit, every
|
|
1520
|
+
* dropped socket and every timeout a Pi provider ever reports permanently
|
|
1521
|
+
* non-retryable — dsh-llm-retry would simply not recognize it. The quota and
|
|
1522
|
+
* context-window predicates it defers to are DSH's own, imported above.
|
|
1523
|
+
* @param message - the provider's failure text.
|
|
1524
|
+
* @returns the harness failure code.
|
|
1525
|
+
*/
|
|
1526
|
+
function classifyPiFailure(message) {
|
|
1527
|
+
if (/\b(?:401|403)\b/.test(message)) return "AUTH";
|
|
1528
|
+
if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
|
|
1529
|
+
if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
|
|
1530
|
+
if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
|
|
1531
|
+
if (/\b5\d\d\b/.test(message)) return "SERVER";
|
|
1532
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return "TIMEOUT";
|
|
1533
|
+
if (/stream ended (?:before|without)\b/i.test(message)) return "TRANSPORT";
|
|
1534
|
+
if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(message) || /\b(?:other side closed|HTTP2 request did not get a response|WebSocket closed unexpectedly)\b/i.test(message) || /\bterminated\b|premature close/i.test(message)) return "TRANSPORT";
|
|
1535
|
+
return "PI_AI_ERROR";
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Pi usage counts → DSH's TokenUsage, mirroring the official adapter's
|
|
1539
|
+
* `mapUsage`: Pi reports zeros rather than absence, so the cache fields appear
|
|
1540
|
+
* only when non-zero.
|
|
1541
|
+
* @param usage - the usage record on a terminal Pi event, if the package sent one.
|
|
1542
|
+
* @returns the harness counts.
|
|
1543
|
+
*/
|
|
1544
|
+
function piUsageToDsh(usage) {
|
|
1545
|
+
const cacheRead = Number(usage?.cacheRead ?? 0);
|
|
1546
|
+
const cacheWrite = Number(usage?.cacheWrite ?? 0);
|
|
1547
|
+
return {
|
|
1548
|
+
inputTokens: Number(usage?.input ?? 0),
|
|
1549
|
+
outputTokens: Number(usage?.output ?? 0),
|
|
1550
|
+
...cacheRead > 0 ? { cacheReadTokens: cacheRead } : {},
|
|
1551
|
+
...cacheWrite > 0 ? { cacheWriteTokens: cacheWrite } : {}
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* The assistant message Pi's terminal `error` event carries.
|
|
1556
|
+
*
|
|
1557
|
+
* Pi delivers failures in-stream as `{ type: 'error', reason, error }` where
|
|
1558
|
+
* `error` is an **AssistantMessage**, not an Error — its text lives on
|
|
1559
|
+
* `errorMessage`. A package that hands back a bare Error instead is met
|
|
1560
|
+
* halfway rather than stringified into "[object Object]".
|
|
1561
|
+
* @param error - the event's payload.
|
|
1562
|
+
* @param fallbackStopReason - the event's own `reason`, used when the payload states none.
|
|
1563
|
+
* @returns a message shape the finish-reason mapping can read.
|
|
1564
|
+
*/
|
|
1565
|
+
function piErrorPayload(error, fallbackStopReason) {
|
|
1566
|
+
if (error instanceof Error) return {
|
|
1567
|
+
stopReason: fallbackStopReason,
|
|
1568
|
+
errorMessage: error.message
|
|
1569
|
+
};
|
|
1570
|
+
if (typeof error === "object" && error !== null) {
|
|
1571
|
+
const record = error;
|
|
1572
|
+
if (typeof record.stopReason === "string" || typeof record.errorMessage === "string") return {
|
|
1573
|
+
stopReason: fallbackStopReason,
|
|
1574
|
+
...record
|
|
1575
|
+
};
|
|
1576
|
+
return {
|
|
1577
|
+
stopReason: fallbackStopReason,
|
|
1578
|
+
errorMessage: JSON.stringify(error)
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
stopReason: fallbackStopReason,
|
|
1583
|
+
errorMessage: String(error ?? "pi2dsh: the package reported a stream failure with no detail")
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* A terminal Pi assistant message → DSH's finish reason, mirroring the
|
|
1588
|
+
* official adapter's `mapStopReason`, including its context-overflow and
|
|
1589
|
+
* empty-response detection (the overflow predicate is Pi's own, vendored).
|
|
1590
|
+
*
|
|
1591
|
+
* One documented deviation: DSH's adapter can trust pi-ai to populate
|
|
1592
|
+
* `content`, and reads an empty array as a degenerate empty completion. This
|
|
1593
|
+
* bridge takes its events from an arbitrary third-party package, so it also
|
|
1594
|
+
* accepts the blocks it actually saw stream past as proof of content. That
|
|
1595
|
+
* only ever narrows the error — a stream that produced output is never
|
|
1596
|
+
* mislabelled EMPTY_RESPONSE because its terminal event omitted a field.
|
|
1597
|
+
* @param message - the assistant message on the `done` or `error` event.
|
|
1598
|
+
* @param contextWindow - the model's resolved capacity, for usage-based overflow detection.
|
|
1599
|
+
* @param sawContent - whether any content block streamed before the terminal event.
|
|
1600
|
+
* @returns the harness finish reason.
|
|
1601
|
+
*/
|
|
1602
|
+
function piFinishReason(message, contextWindow, sawContent) {
|
|
1603
|
+
const stopReason = String(message?.stopReason ?? "stop");
|
|
1604
|
+
const errorMessage = typeof message?.errorMessage === "string" ? message.errorMessage : void 0;
|
|
1605
|
+
const model = String(message?.model ?? "unknown");
|
|
1606
|
+
const usage = message?.usage;
|
|
1607
|
+
const probe = {
|
|
1608
|
+
stopReason,
|
|
1609
|
+
errorMessage,
|
|
1610
|
+
usage: {
|
|
1611
|
+
input: Number(usage?.input ?? 0),
|
|
1612
|
+
output: Number(usage?.output ?? 0),
|
|
1613
|
+
cacheRead: Number(usage?.cacheRead ?? 0),
|
|
1614
|
+
cacheWrite: Number(usage?.cacheWrite ?? 0)
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
if (isContextOverflow(probe, contextWindow) || stopReason === "error" && errorMessage !== void 0 && isContextWindowExceededError(errorMessage)) return {
|
|
1618
|
+
kind: "error",
|
|
1619
|
+
failure: {
|
|
1620
|
+
message: errorMessage ?? `pi2dsh: the package's provider reported context overflow for model "${model}"`,
|
|
1621
|
+
code: CONTEXT_WINDOW_EXCEEDED_CODE
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
switch (stopReason) {
|
|
1625
|
+
case "length": return { kind: "max-tokens" };
|
|
1626
|
+
case "toolUse": return { kind: "tool-calls" };
|
|
1627
|
+
case "aborted": return {
|
|
1628
|
+
kind: "aborted",
|
|
1629
|
+
failure: {
|
|
1630
|
+
message: errorMessage ?? "pi2dsh: the package aborted the stream",
|
|
1631
|
+
code: "ABORTED"
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
case "error": {
|
|
1635
|
+
const text = errorMessage ?? "pi2dsh: the package reported a stream failure";
|
|
1636
|
+
return {
|
|
1637
|
+
kind: "error",
|
|
1638
|
+
failure: {
|
|
1639
|
+
message: text,
|
|
1640
|
+
code: classifyPiFailure(text)
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
default:
|
|
1645
|
+
if ((Array.isArray(message?.content) ? message.content : []).length === 0 && !sawContent) return {
|
|
1646
|
+
kind: "error",
|
|
1647
|
+
failure: {
|
|
1648
|
+
message: `model "${model}" returned a completed response with no content`,
|
|
1649
|
+
code: EMPTY_RESPONSE_CODE
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
return { kind: "stop" };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* The id and name of the tool call an event is opening or extending. Pi states
|
|
1657
|
+
* them on the partial message's content at the event's index, never on the
|
|
1658
|
+
* event itself.
|
|
1659
|
+
* @param event - a `toolcall_start` or `toolcall_delta` event.
|
|
1660
|
+
* @param index - the event's content index.
|
|
1661
|
+
* @returns the call's identity, or undefined when the partial states none.
|
|
1662
|
+
*/
|
|
1663
|
+
function partialToolCall(event, index) {
|
|
1664
|
+
const partial = event.partial;
|
|
1665
|
+
const block = (Array.isArray(partial?.content) ? partial.content : [])[index];
|
|
1666
|
+
if (block === void 0 || block.type !== "toolCall") return void 0;
|
|
1667
|
+
const name = typeof block.name === "string" && block.name.length > 0 ? block.name : void 0;
|
|
1668
|
+
return {
|
|
1669
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
1670
|
+
...name === void 0 ? {} : { name }
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1140
1673
|
/** A Pi AssistantMessageEventStream → DSH StreamChunk sequence. */
|
|
1141
|
-
async function* piEventsToDshChunks(events) {
|
|
1142
|
-
|
|
1674
|
+
async function* piEventsToDshChunks(events, contextWindow) {
|
|
1675
|
+
const openToolCalls = /* @__PURE__ */ new Map();
|
|
1676
|
+
let sawContent = false;
|
|
1143
1677
|
for await (const event of events) {
|
|
1144
1678
|
const type = String(event.type ?? "");
|
|
1145
1679
|
const index = Number(event.contentIndex ?? 0);
|
|
1680
|
+
if (type === "text_start" || type === "thinking_start" || type === "toolcall_start") sawContent = true;
|
|
1146
1681
|
if (type === "text_start") yield {
|
|
1147
1682
|
type: "block-start",
|
|
1148
1683
|
index,
|
|
@@ -1179,18 +1714,24 @@ async function* piEventsToDshChunks(events) {
|
|
|
1179
1714
|
text: String(event.content ?? "")
|
|
1180
1715
|
}
|
|
1181
1716
|
};
|
|
1182
|
-
else if (type === "toolcall_start")
|
|
1183
|
-
|
|
1184
|
-
index,
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1717
|
+
else if (type === "toolcall_start") {
|
|
1718
|
+
const opened = partialToolCall(event, index);
|
|
1719
|
+
if (opened !== void 0) openToolCalls.set(index, opened);
|
|
1720
|
+
yield {
|
|
1721
|
+
type: "block-start",
|
|
1722
|
+
index,
|
|
1723
|
+
blockType: "tool-call"
|
|
1724
|
+
};
|
|
1725
|
+
} else if (type === "toolcall_delta") {
|
|
1726
|
+
const open = openToolCalls.get(index) ?? partialToolCall(event, index);
|
|
1727
|
+
yield {
|
|
1728
|
+
type: "tool-call-delta",
|
|
1729
|
+
index,
|
|
1730
|
+
id: open?.id ?? "",
|
|
1731
|
+
...open?.name === void 0 ? {} : { name: open.name },
|
|
1732
|
+
argumentsDelta: String(event.delta ?? "")
|
|
1733
|
+
};
|
|
1734
|
+
} else if (type === "toolcall_end") {
|
|
1194
1735
|
const call = event.toolCall;
|
|
1195
1736
|
yield {
|
|
1196
1737
|
type: "block-end",
|
|
@@ -1204,61 +1745,34 @@ async function* piEventsToDshChunks(events) {
|
|
|
1204
1745
|
};
|
|
1205
1746
|
} else if (type === "done") {
|
|
1206
1747
|
const message = event.message;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
usage
|
|
1210
|
-
|
|
1211
|
-
outputTokens: Number(piUsage.output ?? 0),
|
|
1212
|
-
...piUsage.cacheRead === void 0 ? {} : { cacheReadTokens: Number(piUsage.cacheRead) },
|
|
1213
|
-
...piUsage.cacheWrite === void 0 ? {} : { cacheWriteTokens: Number(piUsage.cacheWrite) }
|
|
1214
|
-
};
|
|
1215
|
-
yield {
|
|
1216
|
-
type: "usage",
|
|
1217
|
-
usage
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
const stop = String(message?.stopReason ?? "stop");
|
|
1748
|
+
yield {
|
|
1749
|
+
type: "usage",
|
|
1750
|
+
usage: piUsageToDsh(message?.usage)
|
|
1751
|
+
};
|
|
1221
1752
|
yield {
|
|
1222
1753
|
type: "finish",
|
|
1223
|
-
reason:
|
|
1224
|
-
kind: "aborted",
|
|
1225
|
-
failure: {
|
|
1226
|
-
message: "aborted by caller",
|
|
1227
|
-
code: "ABORTED"
|
|
1228
|
-
}
|
|
1229
|
-
} : { kind: "stop" }
|
|
1754
|
+
reason: piFinishReason(message, contextWindow, sawContent)
|
|
1230
1755
|
};
|
|
1231
1756
|
return;
|
|
1232
1757
|
} else if (type === "error") {
|
|
1233
|
-
const
|
|
1758
|
+
const message = piErrorPayload(event.error, String(event.reason ?? "error"));
|
|
1759
|
+
yield {
|
|
1760
|
+
type: "usage",
|
|
1761
|
+
usage: piUsageToDsh(message.usage)
|
|
1762
|
+
};
|
|
1234
1763
|
yield {
|
|
1235
1764
|
type: "finish",
|
|
1236
|
-
reason:
|
|
1237
|
-
kind: String(event.reason ?? "") === "aborted" ? "aborted" : "error",
|
|
1238
|
-
failure: {
|
|
1239
|
-
message: error instanceof Error ? error.message : String(error ?? "provider stream error"),
|
|
1240
|
-
code: "PI_PROVIDER"
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1765
|
+
reason: piFinishReason(message, contextWindow, sawContent)
|
|
1243
1766
|
};
|
|
1244
1767
|
return;
|
|
1245
1768
|
}
|
|
1246
1769
|
}
|
|
1247
|
-
|
|
1248
|
-
type: "finish",
|
|
1249
|
-
reason: {
|
|
1250
|
-
kind: "error",
|
|
1251
|
-
failure: {
|
|
1252
|
-
message: "pi2dsh: Pi provider stream ended without done/error",
|
|
1253
|
-
code: "PI_PROVIDER"
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1770
|
+
throw new LlmError("pi2dsh: the package's provider stream ended without a done or error event", "STREAM_CLOSED");
|
|
1257
1771
|
}
|
|
1258
1772
|
//#endregion
|
|
1259
1773
|
//#region src/provider-adapter.ts
|
|
1260
1774
|
function providerCarriesTransport(value) {
|
|
1261
|
-
return typeof value?.stream === "function";
|
|
1775
|
+
return typeof value?.streamSimple === "function" || typeof value?.stream === "function";
|
|
1262
1776
|
}
|
|
1263
1777
|
function providerModels(provider) {
|
|
1264
1778
|
try {
|
|
@@ -1329,6 +1843,12 @@ function piCarriedFields(model) {
|
|
|
1329
1843
|
* Structural typing carries it through `llm.registerAdapter`.
|
|
1330
1844
|
*/
|
|
1331
1845
|
function piProviderDshAdapter(providerId, provider, host) {
|
|
1846
|
+
let warnedStop = false;
|
|
1847
|
+
const warnStopUnsupported = () => {
|
|
1848
|
+
if (warnedStop) return;
|
|
1849
|
+
warnedStop = true;
|
|
1850
|
+
host.warn(`[pi2dsh] a request to Pi provider ${JSON.stringify(providerId)} carries stop sequences, which Pi models at no layer — they cannot be forwarded, so the model will not halt on them`);
|
|
1851
|
+
};
|
|
1332
1852
|
return {
|
|
1333
1853
|
providerInfo: (id) => ({
|
|
1334
1854
|
id,
|
|
@@ -1362,7 +1882,14 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1362
1882
|
id: modelId,
|
|
1363
1883
|
provider: providerId
|
|
1364
1884
|
};
|
|
1365
|
-
const
|
|
1885
|
+
const carriesImage = (Array.isArray(options.messages) ? options.messages : []).some((message) => blocksContainImage(message.content));
|
|
1886
|
+
if (carriesImage) {
|
|
1887
|
+
if (!(Array.isArray(model.input) ? model.input : []).includes("image")) throw new LlmError(`pi2dsh: model "${modelId}" does not declare image input`, "UNSUPPORTED_CONTENT");
|
|
1888
|
+
}
|
|
1889
|
+
const attachments = carriesImage ? host.resolveAttachments?.() : void 0;
|
|
1890
|
+
if (carriesImage && attachments === void 0) throw new LlmError("pi2dsh: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1891
|
+
const piContext = await dshRequestToPiContext(options, attachments);
|
|
1892
|
+
const effort = typeof options.reasoningEffort === "string" ? options.reasoningEffort : void 0;
|
|
1366
1893
|
const piOptions = {
|
|
1367
1894
|
...auth?.apiKey === void 0 ? {} : { apiKey: auth.apiKey },
|
|
1368
1895
|
...auth?.baseUrl === void 0 ? {} : { baseUrl: auth.baseUrl },
|
|
@@ -1370,9 +1897,20 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1370
1897
|
...options.signal instanceof AbortSignal ? { signal: options.signal } : {},
|
|
1371
1898
|
...typeof options.maxTokens === "number" ? { maxTokens: options.maxTokens } : {},
|
|
1372
1899
|
...typeof options.temperature === "number" ? { temperature: options.temperature } : {},
|
|
1373
|
-
...typeof options.
|
|
1900
|
+
...typeof options.sessionId === "string" ? { sessionId: options.sessionId } : {},
|
|
1901
|
+
...effort === void 0 || effort === "off" ? {} : { reasoning: effort }
|
|
1374
1902
|
};
|
|
1375
|
-
|
|
1903
|
+
const contextWindow = typeof model.contextWindow === "number" ? model.contextWindow : void 0;
|
|
1904
|
+
if (Array.isArray(options.stop) && options.stop.length > 0) warnStopUnsupported();
|
|
1905
|
+
if (typeof provider.streamSimple === "function") {
|
|
1906
|
+
yield* piEventsToDshChunks(provider.streamSimple(model, piContext, piOptions), contextWindow);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const { reasoning, ...rest } = piOptions;
|
|
1910
|
+
yield* piEventsToDshChunks(provider.stream(model, piContext, reasoning === void 0 ? rest : {
|
|
1911
|
+
...rest,
|
|
1912
|
+
reasoningEffort: reasoning
|
|
1913
|
+
}), contextWindow);
|
|
1376
1914
|
}
|
|
1377
1915
|
};
|
|
1378
1916
|
}
|
|
@@ -1472,6 +2010,8 @@ function registerPiProviderRoute(options) {
|
|
|
1472
2010
|
}
|
|
1473
2011
|
//#endregion
|
|
1474
2012
|
//#region src/runtime.ts
|
|
2013
|
+
/** Fallback thread ids when a child session reports none. */
|
|
2014
|
+
let sidePanelSerial = 0;
|
|
1475
2015
|
function logger(ctx) {
|
|
1476
2016
|
const candidate = ctx.logger;
|
|
1477
2017
|
return {
|
|
@@ -1786,11 +2326,12 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
1786
2326
|
const notices = [];
|
|
1787
2327
|
const userQuestions = optionalService(ctx, "userQuestions");
|
|
1788
2328
|
const ui = {
|
|
1789
|
-
notify(message) {
|
|
2329
|
+
notify(message, type) {
|
|
1790
2330
|
const text = String(message);
|
|
1791
2331
|
notices.push(text);
|
|
1792
2332
|
state.notifications.push(text);
|
|
1793
|
-
|
|
2333
|
+
const level = type === "warning" || type === "error" ? "warn" : "info";
|
|
2334
|
+
logger(ctx)[level](`[pi2dsh] ${text}`);
|
|
1794
2335
|
},
|
|
1795
2336
|
select: (title, options) => askOne(ctx, agent, signal, {
|
|
1796
2337
|
id: "pi2dsh-select",
|
|
@@ -2003,9 +2544,18 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2003
2544
|
};
|
|
2004
2545
|
const contextCwd = typeof (sessionOverride?.header)?.cwd === "string" ? sessionOverride.header.cwd : cwdOf(agent);
|
|
2005
2546
|
const base = {
|
|
2547
|
+
...sessionOverride === void 0 ? {} : {
|
|
2548
|
+
sendMessage: (message, options = {}) => sendPiMessage(ctx, state, message.content, deliveryMode(options), sessionOverride, typeof message.customType === "string" ? message.customType : void 0),
|
|
2549
|
+
sendUserMessage: (content) => sendPiMessage(ctx, state, content, "followup", sessionOverride),
|
|
2550
|
+
appendEntry: (customType, data) => {
|
|
2551
|
+
state.bridge.appendCustomEntry(String(sessionOverride.id ?? ""), customType, data);
|
|
2552
|
+
}
|
|
2553
|
+
},
|
|
2006
2554
|
ui,
|
|
2007
2555
|
mode: "rpc",
|
|
2008
|
-
hasUI
|
|
2556
|
+
get hasUI() {
|
|
2557
|
+
return humanAnswererAvailable(userQuestions, agent);
|
|
2558
|
+
},
|
|
2009
2559
|
cwd: contextCwd,
|
|
2010
2560
|
sessionManager: session === void 0 ? state.bridge.readonlySessionManager({
|
|
2011
2561
|
id: "pi2dsh-detached",
|
|
@@ -2013,7 +2563,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2013
2563
|
}, contextCwd) : state.bridge.readonlySessionManager(session, contextCwd),
|
|
2014
2564
|
modelRegistry,
|
|
2015
2565
|
model: agent === void 0 ? void 0 : currentPiModel(state, agent),
|
|
2016
|
-
scopedModels:
|
|
2566
|
+
scopedModels: [],
|
|
2017
2567
|
thinkingLevel: thinkingLevelOf(state, agent),
|
|
2018
2568
|
isIdle: () => command,
|
|
2019
2569
|
isProjectTrusted: () => false,
|
|
@@ -2026,7 +2576,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2026
2576
|
reason: "pi2dsh: aborted by migrated Pi extension"
|
|
2027
2577
|
});
|
|
2028
2578
|
},
|
|
2029
|
-
hasPendingMessages: () =>
|
|
2579
|
+
hasPendingMessages: () => agent?.inbox?.hasPending === true,
|
|
2030
2580
|
shutdown: () => {
|
|
2031
2581
|
capabilityLedgerOf(ctx, state).reportHostDecision({
|
|
2032
2582
|
capability: "ctx.shutdown",
|
|
@@ -2228,39 +2778,93 @@ async function dispatch(state, eventName, event, eventContext) {
|
|
|
2228
2778
|
for (const handler of state.handlers.get(eventName) ?? []) results.push(await state.agentScope.run(agent, () => handler(event, eventContext)));
|
|
2229
2779
|
return results;
|
|
2230
2780
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2781
|
+
/**
|
|
2782
|
+
* What triggered a compaction, in Pi's vocabulary.
|
|
2783
|
+
*
|
|
2784
|
+
* DSH's durable lifecycle events say two things about the trigger: a manual
|
|
2785
|
+
* compaction runs with no open turn (`turn: null`), and one a command drove
|
|
2786
|
+
* cites that command. Both are Pi's "manual".
|
|
2787
|
+
*
|
|
2788
|
+
* The limit, stated rather than papered over: DSH's automatic trigger is
|
|
2789
|
+
* `'pressure' | 'context-overflow'` at the call site but is NOT written to the
|
|
2790
|
+
* log, so Pi's `threshold` and `overflow` cannot be told apart after the fact.
|
|
2791
|
+
* Automatic compactions report `threshold`, the far more common of the two —
|
|
2792
|
+
* and `willRetry` stays false for the same reason. A package keying behavior
|
|
2793
|
+
* on `overflow` specifically will not see it.
|
|
2794
|
+
* @param data - the `compaction/start` or `compaction/summary` event data.
|
|
2795
|
+
*/
|
|
2796
|
+
function compactionReason(data) {
|
|
2797
|
+
if (data.sourceCommandId !== void 0) return "manual";
|
|
2798
|
+
return data.turn === null ? "manual" : "threshold";
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* DSH content → the Pi content blocks packages read.
|
|
2802
|
+
*
|
|
2803
|
+
* Asynchronous because Pi's image block carries the bytes inline while DSH's
|
|
2804
|
+
* carries only an attachment reference, and the bytes come from the attachment
|
|
2805
|
+
* service. The sync version this replaced projected an image as a bare
|
|
2806
|
+
* `{type:'image'}` — a block that announces an image and contains none, which
|
|
2807
|
+
* is the exact shape a package cannot tell from a real one.
|
|
2808
|
+
* @param ctx - context used to reach the attachment service.
|
|
2809
|
+
* @param content - the DSH blocks to project.
|
|
2810
|
+
*/
|
|
2811
|
+
async function dshToPiContent(ctx, content) {
|
|
2812
|
+
const out = [];
|
|
2813
|
+
for (const block of content) if (block.type === "text") out.push({
|
|
2814
|
+
type: "text",
|
|
2815
|
+
text: block.text
|
|
2816
|
+
});
|
|
2817
|
+
else if (block.type === "reasoning") out.push({
|
|
2818
|
+
type: "thinking",
|
|
2819
|
+
thinking: block.text
|
|
2248
2820
|
});
|
|
2821
|
+
else if (block.type === "tool-call") out.push({
|
|
2822
|
+
type: "toolCall",
|
|
2823
|
+
id: block.id,
|
|
2824
|
+
name: block.name,
|
|
2825
|
+
arguments: block.arguments
|
|
2826
|
+
});
|
|
2827
|
+
else if (block.type === "image") {
|
|
2828
|
+
const image = await piImageBlock(ctx, block.attachment);
|
|
2829
|
+
if (image !== void 0) out.push(image);
|
|
2830
|
+
} else out.push({ type: block.type });
|
|
2831
|
+
return out;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* One DSH image attachment as Pi's inline image block.
|
|
2835
|
+
* @param ctx - context used to reach the attachment service.
|
|
2836
|
+
* @param attachment - the durable reference on an image block.
|
|
2837
|
+
* @returns the Pi block, or undefined when the bytes cannot be read.
|
|
2838
|
+
*/
|
|
2839
|
+
async function piImageBlock(ctx, attachment) {
|
|
2840
|
+
if (attachment === void 0 || attachment === null) return void 0;
|
|
2841
|
+
const attachments = optionalService(ctx, "attachments");
|
|
2842
|
+
if (attachments === void 0) return void 0;
|
|
2843
|
+
try {
|
|
2844
|
+
const stored = await attachments.readImage(attachment);
|
|
2845
|
+
return {
|
|
2846
|
+
type: "image",
|
|
2847
|
+
data: Buffer.from(stored.data).toString("base64"),
|
|
2848
|
+
mimeType: String(attachment.mediaType ?? "image/png")
|
|
2849
|
+
};
|
|
2850
|
+
} catch {
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2249
2853
|
}
|
|
2250
|
-
function messageFromSessionEvent(event) {
|
|
2854
|
+
async function messageFromSessionEvent(ctx, event) {
|
|
2251
2855
|
const type = event.type;
|
|
2252
2856
|
const data = event.data;
|
|
2253
2857
|
if (typeof data !== "object" || data === null) return void 0;
|
|
2254
2858
|
const record = data;
|
|
2255
2859
|
if (type === "user/message") return {
|
|
2256
2860
|
role: "user",
|
|
2257
|
-
content: dshToPiContent(record.content ?? [])
|
|
2861
|
+
content: await dshToPiContent(ctx, record.content ?? [])
|
|
2258
2862
|
};
|
|
2259
2863
|
if (type === "assistant/message") {
|
|
2260
2864
|
const message = record.message;
|
|
2261
2865
|
return {
|
|
2262
2866
|
role: "assistant",
|
|
2263
|
-
content: dshToPiContent(message?.content ?? [])
|
|
2867
|
+
content: await dshToPiContent(ctx, message?.content ?? [])
|
|
2264
2868
|
};
|
|
2265
2869
|
}
|
|
2266
2870
|
if (type === "tool/result") {
|
|
@@ -2268,7 +2872,7 @@ function messageFromSessionEvent(event) {
|
|
|
2268
2872
|
return {
|
|
2269
2873
|
role: "toolResult",
|
|
2270
2874
|
toolCallId: tool?.toolCallId,
|
|
2271
|
-
content: dshToPiContent(tool?.content ?? []),
|
|
2875
|
+
content: await dshToPiContent(ctx, tool?.content ?? []),
|
|
2272
2876
|
isError: tool?.isError === true
|
|
2273
2877
|
};
|
|
2274
2878
|
}
|
|
@@ -2315,13 +2919,42 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2315
2919
|
const eventContext = contextFor(ctx, state, agent, void 0);
|
|
2316
2920
|
const type = event.type;
|
|
2317
2921
|
if (type === "turn/start") {
|
|
2318
|
-
|
|
2922
|
+
if (session !== void 0) state.piTurnIndex.set(session, 0);
|
|
2319
2923
|
dispatch(state, "agent_start", { type: "agent_start" }, eventContext).catch((error) => warn("agent_start", error));
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2924
|
+
}
|
|
2925
|
+
if (type === "step/start") dispatch(state, "turn_start", {
|
|
2926
|
+
type: "turn_start",
|
|
2927
|
+
turnIndex: state.piTurnIndex.get(session) ?? 0,
|
|
2928
|
+
timestamp: event.time ?? Date.now()
|
|
2929
|
+
}, eventContext).catch((error) => warn("turn_start", error));
|
|
2930
|
+
if (type === "step/end") {
|
|
2931
|
+
const data = event.data;
|
|
2932
|
+
const turn = Number(data.turn ?? 1);
|
|
2933
|
+
const step = Number(data.step ?? 1);
|
|
2934
|
+
const key = session;
|
|
2935
|
+
const index = state.piTurnIndex.get(key) ?? 0;
|
|
2936
|
+
state.piTurnIndex.set(key, index + 1);
|
|
2937
|
+
state.projection = state.projection.then(async () => {
|
|
2938
|
+
const stepEvents = (session.events ?? []).filter((entry) => {
|
|
2939
|
+
const entryData = entry.data;
|
|
2940
|
+
return Number(entryData?.turn ?? -1) === turn && Number(entryData?.step ?? -1) === step;
|
|
2941
|
+
});
|
|
2942
|
+
const toolResults = (await Promise.all(stepEvents.filter((entry) => entry.type === "tool/result").map((entry) => messageFromSessionEvent(ctx, entry)))).filter((message) => message !== void 0);
|
|
2943
|
+
const assistant = stepEvents.findLast((entry) => entry.type === "assistant/message");
|
|
2944
|
+
const message = assistant === void 0 ? {
|
|
2945
|
+
role: "assistant",
|
|
2946
|
+
content: []
|
|
2947
|
+
} : await messageFromSessionEvent(ctx, assistant) ?? {
|
|
2948
|
+
role: "assistant",
|
|
2949
|
+
content: []
|
|
2950
|
+
};
|
|
2951
|
+
await dispatch(state, "turn_end", {
|
|
2952
|
+
type: "turn_end",
|
|
2953
|
+
turnIndex: index,
|
|
2954
|
+
message,
|
|
2955
|
+
toolResults
|
|
2956
|
+
}, eventContext);
|
|
2957
|
+
}).catch((error) => warn("turn_end", error));
|
|
2325
2958
|
}
|
|
2326
2959
|
if (type === "tool/call") {
|
|
2327
2960
|
const data = event.data;
|
|
@@ -2372,21 +3005,26 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2372
3005
|
type: "session_before_compact",
|
|
2373
3006
|
preparation: { ...event.data },
|
|
2374
3007
|
branchEntries: [],
|
|
2375
|
-
reason:
|
|
3008
|
+
reason: compactionReason(event.data),
|
|
2376
3009
|
willRetry: false
|
|
2377
3010
|
}, eventContext).catch((error) => warn("session_before_compact", error));
|
|
2378
|
-
if (type === "compaction/
|
|
3011
|
+
if (type === "compaction/summary") {
|
|
2379
3012
|
const data = event.data;
|
|
3013
|
+
const shadowed = data.shadowedRange;
|
|
3014
|
+
const usage = data.usage;
|
|
2380
3015
|
dispatch(state, "session_compact", {
|
|
2381
3016
|
type: "session_compact",
|
|
2382
3017
|
compactionEntry: {
|
|
2383
3018
|
type: "compaction",
|
|
2384
3019
|
id: `dsh-${String(event.seq ?? "")}`,
|
|
2385
|
-
summary:
|
|
2386
|
-
|
|
3020
|
+
summary: textBlocks(data.summary).map((block) => block.text).join("\n"),
|
|
3021
|
+
firstKeptEntryId: typeof shadowed?.end === "number" ? `dsh-${shadowed.end + 1}` : "",
|
|
3022
|
+
tokensBefore: Number(data.shadowedTokenCount ?? 0),
|
|
3023
|
+
...usage === void 0 ? {} : { usage },
|
|
3024
|
+
fromHook: false
|
|
2387
3025
|
},
|
|
2388
3026
|
fromExtension: false,
|
|
2389
|
-
reason:
|
|
3027
|
+
reason: compactionReason(data),
|
|
2390
3028
|
willRetry: false
|
|
2391
3029
|
}, eventContext).catch((error) => warn("session_compact", error));
|
|
2392
3030
|
}
|
|
@@ -2404,29 +3042,25 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2404
3042
|
state.lastLoggedModels.set(agent, String(model));
|
|
2405
3043
|
}
|
|
2406
3044
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
role: "assistant",
|
|
2422
|
-
content: []
|
|
2423
|
-
},
|
|
2424
|
-
toolResults: []
|
|
2425
|
-
}, eventContext).then(() => dispatch(state, "agent_end", {
|
|
3045
|
+
state.projection = state.projection.then(async () => {
|
|
3046
|
+
const message = await messageFromSessionEvent(ctx, event);
|
|
3047
|
+
if (message === void 0) return;
|
|
3048
|
+
await dispatch(state, "message_start", {
|
|
3049
|
+
type: "message_start",
|
|
3050
|
+
message
|
|
3051
|
+
}, eventContext);
|
|
3052
|
+
await dispatch(state, "message_end", {
|
|
3053
|
+
type: "message_end",
|
|
3054
|
+
message
|
|
3055
|
+
}, eventContext);
|
|
3056
|
+
}).catch((error) => warn("message lifecycle", error));
|
|
3057
|
+
if (type === "turn/end") state.projection = state.projection.then(async () => {
|
|
3058
|
+
await dispatch(state, "agent_end", {
|
|
2426
3059
|
type: "agent_end",
|
|
2427
3060
|
messages: []
|
|
2428
|
-
}, eventContext)
|
|
2429
|
-
|
|
3061
|
+
}, eventContext);
|
|
3062
|
+
await dispatch(state, "agent_settled", { type: "agent_settled" }, eventContext);
|
|
3063
|
+
}).catch((error) => warn("turn end lifecycle", error));
|
|
2430
3064
|
});
|
|
2431
3065
|
cordis.on("agent/request", async (payload, next) => {
|
|
2432
3066
|
const config = await next();
|
|
@@ -2442,19 +3076,6 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2442
3076
|
...thinking === void 0 || thinking === "off" ? {} : { reasoningEffort: thinking }
|
|
2443
3077
|
};
|
|
2444
3078
|
});
|
|
2445
|
-
cordis.on("tools/result", (exec, result) => {
|
|
2446
|
-
const agent = exec.agent;
|
|
2447
|
-
dispatch(state, "tool_execution_end", {
|
|
2448
|
-
type: "tool_execution_end",
|
|
2449
|
-
toolCallId: exec.callId,
|
|
2450
|
-
toolName: exec.name,
|
|
2451
|
-
result: {
|
|
2452
|
-
content: dshToPiContent(result.content),
|
|
2453
|
-
details: result.meta ?? null
|
|
2454
|
-
},
|
|
2455
|
-
isError: result.isError
|
|
2456
|
-
}, contextFor(ctx, state, agent, exec.signal)).catch((error) => warn("tool_execution_end", error));
|
|
2457
|
-
});
|
|
2458
3079
|
cordis.effect(() => async () => {
|
|
2459
3080
|
for (const agent of state.activeAgents) if (typeof agent === "object" && agent !== null && !state.disposedAgents.has(agent)) {
|
|
2460
3081
|
state.disposedAgents.add(agent);
|
|
@@ -2472,6 +3093,7 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2472
3093
|
function subscribeInterceptors(ctx, state) {
|
|
2473
3094
|
const cordis = ctx;
|
|
2474
3095
|
cordis.on("tools/pre-execute", async (exec, next) => {
|
|
3096
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2475
3097
|
const input = cloneJson(exec.arguments);
|
|
2476
3098
|
const event = {
|
|
2477
3099
|
type: "tool_call",
|
|
@@ -2487,21 +3109,47 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2487
3109
|
reason: `pi2dsh: a Pi tool_call hook mutated arguments of native DSH tool ${JSON.stringify(exec.name)}; DSH logs arguments before policy, so this mutation cannot be honored`
|
|
2488
3110
|
};
|
|
2489
3111
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
3112
|
+
const agent = exec.agent;
|
|
3113
|
+
const tally = agent === void 0 ? void 0 : state.terminateBatch.get(agent) ?? {
|
|
3114
|
+
calls: 0,
|
|
3115
|
+
terminating: 0
|
|
2493
3116
|
};
|
|
3117
|
+
if (tally !== void 0 && agent !== void 0) {
|
|
3118
|
+
tally.calls += 1;
|
|
3119
|
+
state.terminateBatch.set(agent, tally);
|
|
3120
|
+
}
|
|
3121
|
+
for (const result of results) {
|
|
3122
|
+
if (typeof result !== "object" || result === null) continue;
|
|
3123
|
+
const record = result;
|
|
3124
|
+
if (record.block !== true) continue;
|
|
3125
|
+
if (record.terminate === true && tally !== void 0) tally.terminating += 1;
|
|
3126
|
+
return {
|
|
3127
|
+
kind: "deny",
|
|
3128
|
+
reason: String(record.reason ?? "blocked by migrated Pi tool_call hook")
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
2494
3131
|
return next();
|
|
2495
3132
|
});
|
|
2496
3133
|
cordis.on("tools/post-execute", async (exec, result, next) => {
|
|
3134
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2497
3135
|
const downstream = await next();
|
|
3136
|
+
await dispatch(state, "tool_execution_end", {
|
|
3137
|
+
type: "tool_execution_end",
|
|
3138
|
+
toolCallId: exec.callId,
|
|
3139
|
+
toolName: exec.name,
|
|
3140
|
+
result: {
|
|
3141
|
+
content: await dshToPiContent(ctx, result.content),
|
|
3142
|
+
details: result.meta ?? null
|
|
3143
|
+
},
|
|
3144
|
+
isError: result.isError
|
|
3145
|
+
}, contextFor(ctx, state, exec.agent, exec.signal)).catch((error) => logger(ctx).warn(`[pi2dsh] tool_execution_end handler failed: ${String(error)}`));
|
|
2498
3146
|
if (downstream.kind === "block") return downstream;
|
|
2499
3147
|
const event = {
|
|
2500
3148
|
type: "tool_result",
|
|
2501
3149
|
toolName: exec.name,
|
|
2502
3150
|
toolCallId: exec.callId,
|
|
2503
3151
|
input: cloneJson(exec.arguments),
|
|
2504
|
-
content: dshToPiContent(result.content),
|
|
3152
|
+
content: await dshToPiContent(ctx, result.content),
|
|
2505
3153
|
details: result.meta ?? null,
|
|
2506
3154
|
isError: result.isError,
|
|
2507
3155
|
usage: void 0
|
|
@@ -2517,57 +3165,37 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2517
3165
|
feedback: content
|
|
2518
3166
|
};
|
|
2519
3167
|
if (event.isError === false && result.isError) logger(ctx).warn("[pi2dsh] a Pi tool_result hook attempted to recover a DSH error; error recovery was ignored");
|
|
2520
|
-
if (!jsonEqual(event.content, dshToPiContent(result.content))) return {
|
|
3168
|
+
if (!jsonEqual(event.content, await dshToPiContent(ctx, result.content))) return {
|
|
2521
3169
|
kind: "accept",
|
|
2522
3170
|
content
|
|
2523
3171
|
};
|
|
2524
3172
|
return downstream;
|
|
2525
3173
|
});
|
|
3174
|
+
cordis.on("agent/inbox/claimed", (payload) => {
|
|
3175
|
+
const agent = payload.agent;
|
|
3176
|
+
if (agent === void 0 || isSubagentOrigin(agent)) return;
|
|
3177
|
+
const claimed = state.claimedForStep.get(agent) ?? [];
|
|
3178
|
+
claimed.push({
|
|
3179
|
+
message: payload.message,
|
|
3180
|
+
turn: Number(payload.turn ?? 0)
|
|
3181
|
+
});
|
|
3182
|
+
state.claimedForStep.set(agent, claimed);
|
|
3183
|
+
});
|
|
2526
3184
|
cordis.on("agent/pre-step", async (payload, next) => {
|
|
2527
|
-
const decision = await next();
|
|
2528
|
-
if (decision.kind !== "enter" || payload.step !== 1) return decision;
|
|
2529
|
-
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return decision;
|
|
2530
3185
|
const agent = payload.agent;
|
|
2531
|
-
if (isSubagentOrigin(agent)) return
|
|
3186
|
+
if (isSubagentOrigin(agent)) return next();
|
|
3187
|
+
const tally = state.terminateBatch.get(agent);
|
|
3188
|
+
state.terminateBatch.delete(agent);
|
|
3189
|
+
if (tally !== void 0 && tally.calls > 0 && tally.calls === tally.terminating) {
|
|
3190
|
+
logger(ctx).info(`[pi2dsh] a Pi tool_call hook terminated the turn: all ${tally.calls} call(s) in the batch were blocked with terminate`);
|
|
3191
|
+
return { kind: "reject" };
|
|
3192
|
+
}
|
|
3193
|
+
const decision = await next();
|
|
3194
|
+
if (decision.kind !== "enter") return decision;
|
|
2532
3195
|
const signal = payload.signal;
|
|
2533
3196
|
const messages = decision.messages ?? [];
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
const images = await collectPiImages(ctx, userMessages);
|
|
2537
|
-
state.turnSystemPromptOverrides.delete(agent);
|
|
2538
|
-
const event = {
|
|
2539
|
-
type: "before_agent_start",
|
|
2540
|
-
prompt,
|
|
2541
|
-
...images.length > 0 ? { images } : {},
|
|
2542
|
-
systemPrompt: state.currentSystemPrompt,
|
|
2543
|
-
systemPromptOptions: {}
|
|
2544
|
-
};
|
|
2545
|
-
const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
|
|
2546
|
-
const injected = [];
|
|
2547
|
-
for (const result of results) {
|
|
2548
|
-
if (typeof result !== "object" || result === null) continue;
|
|
2549
|
-
const record = result;
|
|
2550
|
-
if (typeof record.systemPrompt === "string") {
|
|
2551
|
-
state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
|
|
2552
|
-
event.systemPrompt = record.systemPrompt;
|
|
2553
|
-
}
|
|
2554
|
-
const message = record.message;
|
|
2555
|
-
if (message !== void 0) {
|
|
2556
|
-
const content = message.content;
|
|
2557
|
-
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2558
|
-
type: "text",
|
|
2559
|
-
text: content
|
|
2560
|
-
}] : content ?? []);
|
|
2561
|
-
injected.push(createUserMessage({
|
|
2562
|
-
content: blocks,
|
|
2563
|
-
source: {
|
|
2564
|
-
kind: "plugin",
|
|
2565
|
-
plugin: state.messageSource,
|
|
2566
|
-
...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
|
|
2567
|
-
}
|
|
2568
|
-
}));
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
3197
|
+
const injected = state.pendingInjections.get(agent) ?? [];
|
|
3198
|
+
state.pendingInjections.delete(agent);
|
|
2571
3199
|
const stepMessages = injected.length === 0 ? messages : [...messages, ...injected];
|
|
2572
3200
|
const transformed = await applyPiContextTransform(ctx, state, agent, signal, stepMessages);
|
|
2573
3201
|
if (transformed === stepMessages && injected.length === 0) return decision;
|
|
@@ -2576,13 +3204,19 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2576
3204
|
messages: transformed
|
|
2577
3205
|
};
|
|
2578
3206
|
});
|
|
3207
|
+
const systemPrompt = optionalService(ctx, "systemPrompt");
|
|
3208
|
+
if (systemPrompt !== void 0) ctx.effect(() => systemPrompt.section({
|
|
3209
|
+
name: "pi2dsh:tool-guidance",
|
|
3210
|
+
order: 150,
|
|
3211
|
+
text: () => piToolPromptContribution(ctx, state)
|
|
3212
|
+
}));
|
|
2579
3213
|
cordis.on("system-prompt/assemble", async (assembly, assembleContext, next) => {
|
|
2580
3214
|
const downstream = await next();
|
|
2581
|
-
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2582
3215
|
const original = renderPrompt(downstream);
|
|
2583
3216
|
state.currentSystemPrompt = original;
|
|
2584
3217
|
const agent = assembleContext.agent ?? assembleContext.scope;
|
|
2585
3218
|
if (agent !== void 0 && !isSubagentOrigin(agent)) {
|
|
3219
|
+
await runBeforeAgentStart(ctx, state, agent, assembleContext.signal, original);
|
|
2586
3220
|
const override = state.turnSystemPromptOverrides.get(agent);
|
|
2587
3221
|
if (override === void 0) return downstream;
|
|
2588
3222
|
state.currentSystemPrompt = override;
|
|
@@ -2594,6 +3228,7 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2594
3228
|
}]
|
|
2595
3229
|
};
|
|
2596
3230
|
}
|
|
3231
|
+
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2597
3232
|
const event = {
|
|
2598
3233
|
type: "before_agent_start",
|
|
2599
3234
|
prompt: "",
|
|
@@ -2616,9 +3251,69 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2616
3251
|
};
|
|
2617
3252
|
});
|
|
2618
3253
|
}
|
|
2619
|
-
|
|
3254
|
+
/**
|
|
3255
|
+
* Pi's `before_agent_start`, run inside the assembly of the turn it belongs to.
|
|
3256
|
+
*
|
|
3257
|
+
* Fires once per user prompt: only when this step's claim opened a NEW turn,
|
|
3258
|
+
* matching Pi, where the event follows the user's message rather than every
|
|
3259
|
+
* model call. A handler's `systemPrompt` becomes this turn's override (reset
|
|
3260
|
+
* at the next turn, exactly as Pi resets to the base prompt) and a returned
|
|
3261
|
+
* `message` is held for the step that is about to be entered.
|
|
3262
|
+
* @param ctx - context used for attachments and handler dispatch.
|
|
3263
|
+
* @param state - runtime state holding claims, overrides, and injections.
|
|
3264
|
+
* @param agent - the agent whose assembly this is.
|
|
3265
|
+
* @param signal - the turn's control signal, when one exists.
|
|
3266
|
+
* @param assembled - the prompt as assembled, before any package override.
|
|
3267
|
+
*/
|
|
3268
|
+
async function runBeforeAgentStart(ctx, state, agent, signal, assembled) {
|
|
3269
|
+
const claimed = state.claimedForStep.get(agent) ?? [];
|
|
3270
|
+
state.claimedForStep.delete(agent);
|
|
3271
|
+
if (claimed.length === 0) return;
|
|
3272
|
+
const turn = Number(claimed[0]?.turn ?? 0);
|
|
3273
|
+
if (state.promptedTurn.get(agent) === turn) return;
|
|
3274
|
+
state.promptedTurn.set(agent, turn);
|
|
3275
|
+
state.turnSystemPromptOverrides.delete(agent);
|
|
3276
|
+
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return;
|
|
3277
|
+
const userMessages = claimed.map((entry) => entry.message).filter((message) => (message.source?.kind ?? "user") === "user");
|
|
3278
|
+
const prompt = userMessages.flatMap((message) => textBlocks(message.content)).map((block) => block.text).join("\n");
|
|
3279
|
+
const images = await collectPiImages(ctx, userMessages);
|
|
3280
|
+
const event = {
|
|
3281
|
+
type: "before_agent_start",
|
|
3282
|
+
prompt,
|
|
3283
|
+
...images.length > 0 ? { images } : {},
|
|
3284
|
+
systemPrompt: assembled,
|
|
3285
|
+
systemPromptOptions: {}
|
|
3286
|
+
};
|
|
3287
|
+
const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
|
|
3288
|
+
const injected = [];
|
|
3289
|
+
for (const result of results) {
|
|
3290
|
+
if (typeof result !== "object" || result === null) continue;
|
|
3291
|
+
const record = result;
|
|
3292
|
+
if (typeof record.systemPrompt === "string") {
|
|
3293
|
+
state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
|
|
3294
|
+
event.systemPrompt = record.systemPrompt;
|
|
3295
|
+
}
|
|
3296
|
+
const message = record.message;
|
|
3297
|
+
if (message === void 0) continue;
|
|
3298
|
+
const content = message.content;
|
|
3299
|
+
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
3300
|
+
type: "text",
|
|
3301
|
+
text: content
|
|
3302
|
+
}] : content ?? []);
|
|
3303
|
+
injected.push(createUserMessage({
|
|
3304
|
+
content: blocks,
|
|
3305
|
+
source: {
|
|
3306
|
+
kind: "plugin",
|
|
3307
|
+
plugin: state.messageSource,
|
|
3308
|
+
...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
|
|
3309
|
+
}
|
|
3310
|
+
}));
|
|
3311
|
+
}
|
|
3312
|
+
if (injected.length > 0) state.pendingInjections.set(agent, injected);
|
|
3313
|
+
}
|
|
3314
|
+
async function piShapeOfPending(ctx, message) {
|
|
2620
3315
|
const source = message.source;
|
|
2621
|
-
const content = dshToPiContent(message.content ?? []);
|
|
3316
|
+
const content = await dshToPiContent(ctx, message.content ?? []);
|
|
2622
3317
|
return typeof source?.piCustomType === "string" ? {
|
|
2623
3318
|
role: "custom",
|
|
2624
3319
|
customType: source.piCustomType,
|
|
@@ -2633,7 +3328,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2633
3328
|
const session = agentSession(agent);
|
|
2634
3329
|
const history = [];
|
|
2635
3330
|
for (const event of session?.events ?? []) {
|
|
2636
|
-
const projected = messageFromSessionEvent(event);
|
|
3331
|
+
const projected = await messageFromSessionEvent(ctx, event);
|
|
2637
3332
|
if (projected === void 0) continue;
|
|
2638
3333
|
if (event.type === "user/message") {
|
|
2639
3334
|
const customType = (event.data?.source)?.piCustomType;
|
|
@@ -2648,7 +3343,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2648
3343
|
}
|
|
2649
3344
|
history.push(projected);
|
|
2650
3345
|
}
|
|
2651
|
-
const projectedPending = pending.map(piShapeOfPending);
|
|
3346
|
+
const projectedPending = await Promise.all(pending.map((message) => piShapeOfPending(ctx, message)));
|
|
2652
3347
|
const event = {
|
|
2653
3348
|
type: "context",
|
|
2654
3349
|
messages: [...history, ...projectedPending]
|
|
@@ -2695,7 +3390,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2695
3390
|
return rebuilt;
|
|
2696
3391
|
}
|
|
2697
3392
|
async function collectPiImages(ctx, messages) {
|
|
2698
|
-
const attachments = ctx
|
|
3393
|
+
const attachments = optionalService(ctx, "attachments");
|
|
2699
3394
|
if (attachments === void 0) return [];
|
|
2700
3395
|
const images = [];
|
|
2701
3396
|
for (const message of messages) {
|
|
@@ -2919,7 +3614,13 @@ function registerTool(ctx, state, tool) {
|
|
|
2919
3614
|
const mutated = state.argMutations.get(exec);
|
|
2920
3615
|
if (mutated !== void 0) state.argMutations.delete(exec);
|
|
2921
3616
|
const effective = mutated ?? args;
|
|
2922
|
-
const prepared =
|
|
3617
|
+
const prepared = validateToolArguments({
|
|
3618
|
+
name: live.name,
|
|
3619
|
+
parameters: live.parameters
|
|
3620
|
+
}, {
|
|
3621
|
+
name: live.name,
|
|
3622
|
+
arguments: live.prepareArguments?.(cloneJson(effective)) ?? effective
|
|
3623
|
+
});
|
|
2923
3624
|
const agent = exec.agent;
|
|
2924
3625
|
const result = await normalizeToolResultForDsh(ctx, await state.agentScope.run(agent, () => live.execute(String(exec.callId), prepared, exec.signal, (update) => {
|
|
2925
3626
|
dispatch(state, "tool_execution_update", {
|
|
@@ -2957,10 +3658,57 @@ function currentAgent(state) {
|
|
|
2957
3658
|
function toolRuntime(ctx, agent) {
|
|
2958
3659
|
return (agent?.ctx)?.tools ?? ctx.tools;
|
|
2959
3660
|
}
|
|
3661
|
+
/**
|
|
3662
|
+
* The system-prompt text a package's ACTIVE tools contribute.
|
|
3663
|
+
*
|
|
3664
|
+
* Pi renders `promptSnippet` into its "Available tools" list and
|
|
3665
|
+
* `promptGuidelines` into its "Guidelines" bullets. DSH assembles its prompt
|
|
3666
|
+
* from ordered sections, so both arrive as one section in DSH's tool-guidance
|
|
3667
|
+
* band, headed the way Pi heads them.
|
|
3668
|
+
* @param ctx - context used to read which tools are active.
|
|
3669
|
+
* @param state - runtime state holding the registered Pi tools.
|
|
3670
|
+
* @returns the section text, empty when no active Pi tool contributes any.
|
|
3671
|
+
*/
|
|
3672
|
+
function piToolPromptContribution(ctx, state) {
|
|
3673
|
+
const active = new Set(getActiveTools(ctx, state));
|
|
3674
|
+
const snippets = [];
|
|
3675
|
+
const guidelines = [];
|
|
3676
|
+
for (const [name, tool] of state.tools) {
|
|
3677
|
+
if (!active.has(name)) continue;
|
|
3678
|
+
const snippet = tool.promptSnippet;
|
|
3679
|
+
if (typeof snippet === "string" && snippet.trim().length > 0) snippets.push(`- ${snippet.trim()}`);
|
|
3680
|
+
const bullets = tool.promptGuidelines;
|
|
3681
|
+
if (!Array.isArray(bullets)) continue;
|
|
3682
|
+
for (const bullet of bullets) if (typeof bullet === "string" && bullet.trim().length > 0) guidelines.push(`- ${bullet.trim()}`);
|
|
3683
|
+
}
|
|
3684
|
+
const parts = [];
|
|
3685
|
+
if (snippets.length > 0) parts.push(`Available tools:\n${snippets.join("\n")}`);
|
|
3686
|
+
if (guidelines.length > 0) parts.push(`Guidelines:\n${guidelines.join("\n")}`);
|
|
3687
|
+
return parts.join("\n\n");
|
|
3688
|
+
}
|
|
2960
3689
|
function getActiveTools(ctx, state) {
|
|
2961
3690
|
const agent = currentAgent(state);
|
|
2962
3691
|
return toolRuntime(ctx, agent).schemas(agent).map((tool) => tool.name);
|
|
2963
3692
|
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Pi's `setActiveTools`, on DSH's restriction seam.
|
|
3695
|
+
*
|
|
3696
|
+
* Pi walks the requested names and keeps the ones its registry knows —
|
|
3697
|
+
* **unknown names are silently skipped**. DSH's `restrict` instead FAILS the
|
|
3698
|
+
* whole call on a name it cannot restrict (unknown, scope-local, or a
|
|
3699
|
+
* reserved transport name), so passing Pi's list through verbatim turned a
|
|
3700
|
+
* routine Pi call into a hard error over one stale name.
|
|
3701
|
+
*
|
|
3702
|
+
* So the list is narrowed to what DSH says is restrictable before restricting.
|
|
3703
|
+
* The one case that cannot be both: a tool that is VISIBLE but not
|
|
3704
|
+
* restrictable (a scope's own registration, `run_code` outside native mode)
|
|
3705
|
+
* cannot be switched off at all. Silence there would leave a tool running that
|
|
3706
|
+
* the package believes it disabled, so it is reported once — the package's
|
|
3707
|
+
* call still takes effect for everything else.
|
|
3708
|
+
* @param ctx - context used to reach the tool runtime.
|
|
3709
|
+
* @param state - runtime state holding the per-agent restriction disposers.
|
|
3710
|
+
* @param names - the tool names the package wants active.
|
|
3711
|
+
*/
|
|
2964
3712
|
function setActiveTools(ctx, state, names) {
|
|
2965
3713
|
const unique = [...new Set(names)];
|
|
2966
3714
|
const agent = currentAgent(state);
|
|
@@ -2971,27 +3719,104 @@ function setActiveTools(ctx, state, names) {
|
|
|
2971
3719
|
logger(ctx).warn("[pi2dsh] setActiveTools deferred: the current agent exposes no scoped tools.restrict()");
|
|
2972
3720
|
return;
|
|
2973
3721
|
}
|
|
3722
|
+
const restrictable = restrictableToolNames(scopedTools, agent);
|
|
3723
|
+
const allow = restrictable === void 0 ? unique : unique.filter((name) => restrictable.has(name));
|
|
3724
|
+
if (restrictable !== void 0) {
|
|
3725
|
+
const unswitchable = [...new Set(scopedTools.schemas(agent).map((schema) => schema.name))].filter((name) => !restrictable.has(name) && !unique.includes(name));
|
|
3726
|
+
if (unswitchable.length > 0) logger(ctx).warn(`[pi2dsh] setActiveTools could not deactivate ${unswitchable.map((name) => JSON.stringify(name)).join(", ")}: DSH does not allow restricting a scope-registered or reserved tool, so it stays available to the model`);
|
|
3727
|
+
}
|
|
2974
3728
|
state.toolRestrictions.get(agent)?.();
|
|
2975
|
-
state.toolRestrictions.set(agent, scopedTools.restrict({
|
|
3729
|
+
state.toolRestrictions.set(agent, allow.length === 0 ? scopedTools.restrict({ deny: [...restrictable ?? /* @__PURE__ */ new Set()] }) : scopedTools.restrict({ allow }));
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3732
|
+
* The tool names DSH will accept in a restriction for this scope.
|
|
3733
|
+
* @param tools - the agent-scoped tool runtime.
|
|
3734
|
+
* @param agent - the scope key.
|
|
3735
|
+
* @returns the restrictable names, or undefined when this runtime exposes no view.
|
|
3736
|
+
*/
|
|
3737
|
+
function restrictableToolNames(tools, agent) {
|
|
3738
|
+
const view = tools.view;
|
|
3739
|
+
if (typeof view !== "function") return void 0;
|
|
3740
|
+
try {
|
|
3741
|
+
const names = view.call(tools, agent)?.restrictableNames;
|
|
3742
|
+
return names instanceof Set ? names : void 0;
|
|
3743
|
+
} catch {
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
2976
3746
|
}
|
|
2977
3747
|
function deliverAgentMessage(agent, message, mode) {
|
|
2978
3748
|
const deliver = agent[mode];
|
|
2979
3749
|
if (typeof deliver !== "function") throw new Error(`pi2dsh: active DSH agent has no ${mode}() delivery method`);
|
|
2980
3750
|
deliver.call(agent, message);
|
|
2981
3751
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
3752
|
+
/**
|
|
3753
|
+
* Pi's `sendMessage` / `sendUserMessage`, on DSH.
|
|
3754
|
+
*
|
|
3755
|
+
* Two things this has to get right that a plain `deliverAgentMessage` does not:
|
|
3756
|
+
*
|
|
3757
|
+
* - **Durability.** Pi's no-turn `sendMessage` appends to the session and
|
|
3758
|
+
* emits its message events before it returns: on return the message IS in
|
|
3759
|
+
* the conversation. DSH's inject only queues it in the agent's inbox, where
|
|
3760
|
+
* it becomes a `user/message` when the next step claims it — so a turn
|
|
3761
|
+
* cancelled in between dropped it, and a package that had already reported
|
|
3762
|
+
* success was wrong. The no-turn mode now appends to the durable log
|
|
3763
|
+
* itself, which is what Pi's contract promises.
|
|
3764
|
+
* - **Which session.** Inside a `withSession` callback the context is bound to
|
|
3765
|
+
* the REPLACEMENT session, but the live agent is still the one that started
|
|
3766
|
+
* the operation — so routing through the agent wrote into the OLD session.
|
|
3767
|
+
* An override session is written to directly.
|
|
3768
|
+
* @param ctx - context used for content conversion.
|
|
3769
|
+
* @param state - runtime state (message source, active agent).
|
|
3770
|
+
* @param content - the Pi content to deliver.
|
|
3771
|
+
* @param mode - inject (no turn), steer, or followup.
|
|
3772
|
+
* @param sessionOverride - the replacement session, inside a withSession callback.
|
|
3773
|
+
* @param customType - Pi's role:"custom" marker, when the caller sent one.
|
|
3774
|
+
*/
|
|
3775
|
+
async function sendPiMessage(ctx, state, content, mode, sessionOverride, customType) {
|
|
2984
3776
|
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2985
3777
|
type: "text",
|
|
2986
3778
|
text: content
|
|
2987
3779
|
}] : content);
|
|
2988
|
-
|
|
3780
|
+
const message = createUserMessage({
|
|
2989
3781
|
content: blocks,
|
|
2990
3782
|
source: {
|
|
2991
3783
|
kind: "plugin",
|
|
2992
|
-
plugin: state.messageSource
|
|
3784
|
+
plugin: state.messageSource,
|
|
3785
|
+
...customType === void 0 ? {} : { piCustomType: customType }
|
|
2993
3786
|
}
|
|
2994
|
-
})
|
|
3787
|
+
});
|
|
3788
|
+
if (sessionOverride !== void 0) {
|
|
3789
|
+
appendUserMessage(sessionOverride, message);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
if (mode === "inject") {
|
|
3793
|
+
const session = agentSession(currentAgent(state));
|
|
3794
|
+
if (session === void 0) throw new Error("pi2dsh: sendMessage requires one active DSH agent with a durable session");
|
|
3795
|
+
appendUserMessage(session, message);
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
deliverAgentMessage(requireAgent(state, "sendUserMessage"), message, mode);
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Pi's delivery options → this bridge's mode. No options at all means "into
|
|
3802
|
+
* the conversation, no turn", which is Pi's own default.
|
|
3803
|
+
* @param options - the caller's delivery options.
|
|
3804
|
+
*/
|
|
3805
|
+
function deliveryMode(options) {
|
|
3806
|
+
if (options.deliverAs === "steer") return "steer";
|
|
3807
|
+
if (options.deliverAs === "followUp" || options.deliverAs === "nextTurn" || options.triggerTurn === true) return "followup";
|
|
3808
|
+
return "inject";
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Append one plugin-sourced user message to a session's durable log, which is
|
|
3812
|
+
* what makes it part of the conversation before the call returns.
|
|
3813
|
+
* @param session - the live DSH session to append to.
|
|
3814
|
+
* @param message - the message, already in DSH shape.
|
|
3815
|
+
*/
|
|
3816
|
+
function appendUserMessage(session, message) {
|
|
3817
|
+
const append = session.append;
|
|
3818
|
+
if (typeof append !== "function") throw new Error("pi2dsh: this session cannot be appended to, so the message could not be delivered durably");
|
|
3819
|
+
append.call(session, "user/message", message, { surfaceOp: "append" });
|
|
2995
3820
|
}
|
|
2996
3821
|
function combineExecSignal(options) {
|
|
2997
3822
|
const controller = new AbortController();
|
|
@@ -3101,6 +3926,51 @@ function registerCommand(ctx, state, command) {
|
|
|
3101
3926
|
}
|
|
3102
3927
|
logger(ctx).warn(`[pi2dsh] command /${command.name} was not registered: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
3103
3928
|
}
|
|
3929
|
+
/**
|
|
3930
|
+
* The session's current name: DSH's title when a title service is mounted,
|
|
3931
|
+
* this bridge's sidecar otherwise (and as the fallback for a session named
|
|
3932
|
+
* before the service was there).
|
|
3933
|
+
* @param ctx - the cordis context to resolve the optional service from.
|
|
3934
|
+
* @param state - runtime state holding the sidecar.
|
|
3935
|
+
* @param session - the live DSH session.
|
|
3936
|
+
*/
|
|
3937
|
+
function sessionNameOf(ctx, state, session) {
|
|
3938
|
+
const title = optionalService(ctx, "sessionTitle")?.get(session)?.title;
|
|
3939
|
+
return typeof title === "string" && title.length > 0 ? title : state.bridge.getName(session.id);
|
|
3940
|
+
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Whether a human can actually answer a question right now.
|
|
3943
|
+
*
|
|
3944
|
+
* Two facts decide it, and the old check (`the service is mounted`) saw
|
|
3945
|
+
* neither: a headless composition mounts the service and registers NO
|
|
3946
|
+
* provider, and a delegated child agent has no human answerer at all — both
|
|
3947
|
+
* make every `ctx.ui.select/confirm/input` throw for a package that was told
|
|
3948
|
+
* `hasUI: true` and skipped its non-interactive path.
|
|
3949
|
+
*
|
|
3950
|
+
* The provider is a private field, but its existence is publicly observable:
|
|
3951
|
+
* DSH allows exactly one provider per context and refuses a second with
|
|
3952
|
+
* `DUPLICATE_PROVIDER`. So registering a probe answers the question — a
|
|
3953
|
+
* refusal means a real provider is there, and an acceptance means there was
|
|
3954
|
+
* none, which the immediate disposal restores.
|
|
3955
|
+
* @param userQuestions - the mounted question service, if any.
|
|
3956
|
+
* @param agent - the agent whose turn this context belongs to.
|
|
3957
|
+
*/
|
|
3958
|
+
function humanAnswererAvailable(userQuestions, agent) {
|
|
3959
|
+
if (userQuestions === void 0) return false;
|
|
3960
|
+
if (isSubagentOrigin(agent)) return false;
|
|
3961
|
+
const service = userQuestions;
|
|
3962
|
+
if (typeof service.registerProvider !== "function") return false;
|
|
3963
|
+
let dispose;
|
|
3964
|
+
try {
|
|
3965
|
+
dispose = service.registerProvider({ ask: async () => {
|
|
3966
|
+
throw new Error("pi2dsh probe provider");
|
|
3967
|
+
} });
|
|
3968
|
+
} catch {
|
|
3969
|
+
return true;
|
|
3970
|
+
}
|
|
3971
|
+
dispose?.();
|
|
3972
|
+
return false;
|
|
3973
|
+
}
|
|
3104
3974
|
function requireSession(state, operation) {
|
|
3105
3975
|
const session = agentSession(currentAgent(state));
|
|
3106
3976
|
if (session === void 0) throw new Error(`pi2dsh: ${operation} requires one active DSH agent with a durable session`);
|
|
@@ -3149,7 +4019,8 @@ function createPiApi(ctx, state) {
|
|
|
3149
4019
|
providerConfig: value,
|
|
3150
4020
|
store: oauthStoreOf(state)
|
|
3151
4021
|
}),
|
|
3152
|
-
warn: (message) => logger(ctx).warn(message)
|
|
4022
|
+
warn: (message) => logger(ctx).warn(message),
|
|
4023
|
+
resolveAttachments: () => optionalService(ctx, "attachments")
|
|
3153
4024
|
}
|
|
3154
4025
|
});
|
|
3155
4026
|
if (routeDisposer !== void 0) {
|
|
@@ -3204,8 +4075,7 @@ function createPiApi(ctx, state) {
|
|
|
3204
4075
|
},
|
|
3205
4076
|
sendMessage(message, options = {}) {
|
|
3206
4077
|
requireAgent(state, "sendMessage");
|
|
3207
|
-
|
|
3208
|
-
return sendPiMessage(ctx, state, message.content, mode);
|
|
4078
|
+
return sendPiMessage(ctx, state, message.content, deliveryMode(options), void 0, typeof message.customType === "string" ? message.customType : void 0);
|
|
3209
4079
|
},
|
|
3210
4080
|
sendUserMessage(content, options = {}) {
|
|
3211
4081
|
requireAgent(state, "sendUserMessage");
|
|
@@ -3217,15 +4087,17 @@ function createPiApi(ctx, state) {
|
|
|
3217
4087
|
},
|
|
3218
4088
|
setSessionName(name) {
|
|
3219
4089
|
const session = requireSession(state, "setSessionName");
|
|
3220
|
-
|
|
4090
|
+
const titles = optionalService(ctx, "sessionTitle");
|
|
4091
|
+
if (titles !== void 0 && String(name).trim().length > 0) titles.rename(session, String(name));
|
|
4092
|
+
else state.bridge.setName(session.id, String(name));
|
|
3221
4093
|
dispatch(state, "session_info_changed", {
|
|
3222
4094
|
type: "session_info_changed",
|
|
3223
|
-
name: state
|
|
4095
|
+
name: sessionNameOf(ctx, state, session)
|
|
3224
4096
|
}, contextFor(ctx, state, currentAgent(state), void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] session_info_changed handler failed: ${String(error)}`));
|
|
3225
4097
|
},
|
|
3226
4098
|
getSessionName() {
|
|
3227
4099
|
const session = agentSession(currentAgent(state));
|
|
3228
|
-
return session === void 0 ? void 0 : state
|
|
4100
|
+
return session === void 0 ? void 0 : sessionNameOf(ctx, state, session);
|
|
3229
4101
|
},
|
|
3230
4102
|
setLabel(entryId, label) {
|
|
3231
4103
|
const session = requireSession(state, "setLabel");
|
|
@@ -3272,11 +4144,12 @@ function createPiApi(ctx, state) {
|
|
|
3272
4144
|
...typeof model?.id === "string" ? { model: model.id } : {}
|
|
3273
4145
|
};
|
|
3274
4146
|
if (override.model === void 0) return false;
|
|
4147
|
+
const previousModel = currentPiModel(state, agent);
|
|
3275
4148
|
state.modelOverrides.set(agent, override);
|
|
3276
4149
|
dispatch(state, "model_select", {
|
|
3277
4150
|
type: "model_select",
|
|
3278
4151
|
model,
|
|
3279
|
-
previousModel
|
|
4152
|
+
previousModel,
|
|
3280
4153
|
source: "set"
|
|
3281
4154
|
}, contextFor(ctx, state, agent, void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] model_select handler failed: ${String(error)}`));
|
|
3282
4155
|
return true;
|
|
@@ -3474,6 +4347,12 @@ async function applyPiPackage(ctx, options) {
|
|
|
3474
4347
|
modelOverrides: /* @__PURE__ */ new WeakMap(),
|
|
3475
4348
|
thinkingLevels: /* @__PURE__ */ new WeakMap(),
|
|
3476
4349
|
turnSystemPromptOverrides: /* @__PURE__ */ new WeakMap(),
|
|
4350
|
+
projection: Promise.resolve(),
|
|
4351
|
+
terminateBatch: /* @__PURE__ */ new WeakMap(),
|
|
4352
|
+
piTurnIndex: /* @__PURE__ */ new WeakMap(),
|
|
4353
|
+
claimedForStep: /* @__PURE__ */ new WeakMap(),
|
|
4354
|
+
promptedTurn: /* @__PURE__ */ new WeakMap(),
|
|
4355
|
+
pendingInjections: /* @__PURE__ */ new WeakMap(),
|
|
3477
4356
|
globalThinkingLevel: "off",
|
|
3478
4357
|
argMutations: /* @__PURE__ */ new WeakMap(),
|
|
3479
4358
|
streamingTexts: /* @__PURE__ */ new Map(),
|
|
@@ -3518,7 +4397,24 @@ async function applyPiPackage(ctx, options) {
|
|
|
3518
4397
|
}));
|
|
3519
4398
|
ctx.effect(() => () => __setPiAiLlmBridge(void 0));
|
|
3520
4399
|
}
|
|
3521
|
-
|
|
4400
|
+
const sidePanel = state.shared.sidePanel ??= new SidePanelRegistry();
|
|
4401
|
+
if (state.shared.sidePanelRouted !== true) state.shared.sidePanelRouted = registerSidePanelRoute(ctx, sidePanel);
|
|
4402
|
+
__setSubagentSessionFactory(async (subagentOptions) => {
|
|
4403
|
+
const created = await createBridgedAgentSession(subagentHost(), subagentOptions);
|
|
4404
|
+
const parent = agentSession(currentAgent(state));
|
|
4405
|
+
const parentId = parent === void 0 ? "" : String(parent.id ?? "");
|
|
4406
|
+
if (parentId.length > 0) {
|
|
4407
|
+
const dispose = sidePanel.track(parentId, {
|
|
4408
|
+
id: String(created.session.sessionId ?? "") || `pi2dsh-thread-${parentId}-${sidePanelSerial++}`,
|
|
4409
|
+
label: childLabel(subagentOptions.label, state.packageName),
|
|
4410
|
+
package: state.packageName,
|
|
4411
|
+
session: created.session
|
|
4412
|
+
});
|
|
4413
|
+
ctx.effect(() => dispose);
|
|
4414
|
+
}
|
|
4415
|
+
return created;
|
|
4416
|
+
});
|
|
4417
|
+
const subagentHost = () => ({
|
|
3522
4418
|
cordis: ctx,
|
|
3523
4419
|
cwd: () => cwdOf(currentAgent(state)),
|
|
3524
4420
|
parentSessionId: () => {
|
|
@@ -3534,10 +4430,10 @@ async function applyPiPackage(ctx, options) {
|
|
|
3534
4430
|
},
|
|
3535
4431
|
piContentToDsh: (content) => piToDshContent(ctx, content),
|
|
3536
4432
|
deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
|
|
3537
|
-
messageFromSessionEvent,
|
|
4433
|
+
messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
|
|
3538
4434
|
messageSource: state.messageSource,
|
|
3539
4435
|
packageName: state.packageName
|
|
3540
|
-
}
|
|
4436
|
+
});
|
|
3541
4437
|
ctx.effect(() => () => __setSubagentSessionFactory(void 0));
|
|
3542
4438
|
await registerPromptCommands(ctx, state, rootDir, options.manifest);
|
|
3543
4439
|
const onExtensionError = (failure) => logger(ctx).warn(`[pi2dsh] extension entry failed and was skipped (matching Pi's per-extension error isolation): ${failure}`);
|
|
@@ -3570,7 +4466,10 @@ async function applyPiPackage(ctx, options) {
|
|
|
3570
4466
|
logger(ctx).info(`[pi2dsh] loaded ${options.manifest.package.name}: ${state.tools.size} tools, ${state.commands.size} commands, ${options.manifest.skillDirs.length} skill roots${healthSuffix}`);
|
|
3571
4467
|
}
|
|
3572
4468
|
const runtimeInternals = {
|
|
4469
|
+
compactionReason,
|
|
4470
|
+
dshToPiContent,
|
|
3573
4471
|
expandPrompt,
|
|
4472
|
+
isSubagentOrigin,
|
|
3574
4473
|
normalizeToolResult,
|
|
3575
4474
|
splitArguments,
|
|
3576
4475
|
textBlocks
|
|
@@ -3578,4 +4477,4 @@ const runtimeInternals = {
|
|
|
3578
4477
|
//#endregion
|
|
3579
4478
|
export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
|
|
3580
4479
|
|
|
3581
|
-
//# sourceMappingURL=runtime-
|
|
4480
|
+
//# sourceMappingURL=runtime-B_Ig28og.mjs.map
|