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.
Files changed (35) hide show
  1. package/README.md +37 -7
  2. package/README.zh.md +32 -6
  3. package/dist/{analyzer-CsrBeS8r.mjs → analyzer-2Nx5XYOA.mjs} +3 -3
  4. package/dist/{analyzer-CsrBeS8r.mjs.map → analyzer-2Nx5XYOA.mjs.map} +1 -1
  5. package/dist/cli.mjs +7 -44
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client.js +139 -0
  8. package/dist/compat/pi-coding-agent.d.mts +82 -14
  9. package/dist/compat/pi-coding-agent.d.mts.map +1 -1
  10. package/dist/compat/pi-coding-agent.mjs +1 -1
  11. package/dist/credentials-oauth.mjs +1 -1
  12. package/dist/host.d.mts +1 -11
  13. package/dist/host.d.mts.map +1 -1
  14. package/dist/host.mjs +3 -98
  15. package/dist/host.mjs.map +1 -1
  16. package/dist/index.d.mts +18 -19
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +5 -14
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/mcp-config-Cjae0vot.mjs +766 -0
  21. package/dist/mcp-config-Cjae0vot.mjs.map +1 -0
  22. package/dist/{pi-coding-agent-Ctw-BhoY.mjs → pi-coding-agent-CYnmeJEx.mjs} +0 -0
  23. package/dist/pi-coding-agent-CYnmeJEx.mjs.map +1 -0
  24. package/dist/{runtime-Xy8yU8wO.mjs → runtime-B_Ig28og.mjs} +1122 -223
  25. package/dist/runtime-B_Ig28og.mjs.map +1 -0
  26. package/dist/runtime.d.mts +33 -0
  27. package/dist/runtime.d.mts.map +1 -1
  28. package/dist/runtime.mjs +1 -1
  29. package/package.json +23 -9
  30. package/dist/generator-CSOv6wYT.mjs +0 -298
  31. package/dist/generator-CSOv6wYT.mjs.map +0 -1
  32. package/dist/mcp-config-Cn5gWemZ.mjs +0 -707
  33. package/dist/mcp-config-Cn5gWemZ.mjs.map +0 -1
  34. package/dist/pi-coding-agent-Ctw-BhoY.mjs.map +0 -1
  35. 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-Ctw-BhoY.mjs";
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: () => entriesOf().filter((entry) => entry.type === "message" || entry.type === "compaction" || entry.type === "branch_summary" || entry.type === "custom_message"),
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) this.messages.push(message);
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
- const message = this.#host.messageFromSessionEvent(event);
468
- if (message !== void 0) {
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
- emit({
484
- type: "turn_end",
485
- turnIndex: Number(event.data.turn ?? 1) - 1,
486
- message: {
487
- role: "assistant",
488
- content: []
489
- },
490
- toolResults: []
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: `<prior-conversation>\n${rendered.join("\n\n")}\n</prior-conversation>`
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: String(result?.toolCallId ?? ""),
1096
- content: piContentFromDshBlocks(Array.isArray(result?.content) ? result.content : []),
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
- function piContentFromDshBlocks(blocks) {
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
- let usage;
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") yield {
1183
- type: "block-start",
1184
- index,
1185
- blockType: "tool-call"
1186
- };
1187
- else if (type === "toolcall_delta") yield {
1188
- type: "tool-call-delta",
1189
- index,
1190
- id: "",
1191
- argumentsDelta: String(event.delta ?? "")
1192
- };
1193
- else if (type === "toolcall_end") {
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
- const piUsage = message?.usage;
1208
- if (piUsage !== void 0) {
1209
- usage = {
1210
- inputTokens: Number(piUsage.input ?? 0),
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: stop === "length" ? { kind: "max-tokens" } : stop === "toolUse" ? { kind: "tool-calls" } : stop === "aborted" ? {
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 error = event.error;
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
- yield {
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 piContext = dshRequestToPiContext(options);
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.reasoningEffort === "string" ? { reasoningEffort: options.reasoningEffort } : {}
1900
+ ...typeof options.sessionId === "string" ? { sessionId: options.sessionId } : {},
1901
+ ...effort === void 0 || effort === "off" ? {} : { reasoning: effort }
1374
1902
  };
1375
- yield* piEventsToDshChunks(provider.stream(model, piContext, piOptions));
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
- logger(ctx).info(`[pi2dsh] ${text}`);
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: userQuestions !== void 0,
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: catalog?.all() ?? [],
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: () => false,
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
- function dshToPiContent(content) {
2232
- return content.map((block) => {
2233
- if (block.type === "text") return {
2234
- type: "text",
2235
- text: block.text
2236
- };
2237
- if (block.type === "reasoning") return {
2238
- type: "thinking",
2239
- thinking: block.text
2240
- };
2241
- if (block.type === "tool-call") return {
2242
- type: "toolCall",
2243
- id: block.id,
2244
- name: block.name,
2245
- arguments: block.arguments
2246
- };
2247
- return { type: block.type };
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
- const turn = Number(event.data.turn ?? 1);
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
- dispatch(state, "turn_start", {
2321
- type: "turn_start",
2322
- turnIndex: turn - 1,
2323
- timestamp: event.time ?? Date.now()
2324
- }, eventContext).catch((error) => warn("turn_start", error));
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: "threshold",
3008
+ reason: compactionReason(event.data),
2376
3009
  willRetry: false
2377
3010
  }, eventContext).catch((error) => warn("session_before_compact", error));
2378
- if (type === "compaction/end" || type === "compaction/summary") {
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: typeof data.summary === "string" ? data.summary : "",
2386
- ...data
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: "threshold",
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
- const message = messageFromSessionEvent(event);
2408
- if (message !== void 0) dispatch(state, "message_start", {
2409
- type: "message_start",
2410
- message
2411
- }, eventContext).then(() => dispatch(state, "message_end", {
2412
- type: "message_end",
2413
- message
2414
- }, eventContext)).catch((error) => warn("message lifecycle", error));
2415
- if (type === "turn/end") {
2416
- const data = event.data;
2417
- dispatch(state, "turn_end", {
2418
- type: "turn_end",
2419
- turnIndex: Number(data.turn ?? 1) - 1,
2420
- message: {
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)).then(() => dispatch(state, "agent_settled", { type: "agent_settled" }, eventContext)).catch((error) => warn("turn end lifecycle", error));
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
- for (const result of results) if (typeof result === "object" && result !== null && result.block === true) return {
2491
- kind: "deny",
2492
- reason: String(result.reason ?? "blocked by migrated Pi tool_call hook")
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 decision;
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 userMessages = messages.filter((message) => (message.source?.kind ?? "user") === "user");
2535
- const prompt = userMessages.flatMap((message) => textBlocks(message.content)).map((block) => block.text).join("\n");
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
- function piShapeOfPending(message) {
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.get("attachments");
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 = live.prepareArguments?.(cloneJson(effective)) ?? effective;
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({ allow: unique }));
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
- async function sendPiMessage(ctx, state, content, mode) {
2983
- const agent = requireAgent(state, mode === "inject" ? "sendMessage" : "sendUserMessage");
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
- deliverAgentMessage(agent, createUserMessage({
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
- }), mode);
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
- const mode = options.deliverAs === "steer" ? "steer" : options.deliverAs === "followUp" || options.deliverAs === "nextTurn" || options.triggerTurn === true ? "followup" : "inject";
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
- state.bridge.setName(session.id, String(name));
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.bridge.getName(session.id)
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.bridge.getName(session.id);
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: state.modelOverrides.get(agent),
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
- __setSubagentSessionFactory((subagentOptions) => createBridgedAgentSession({
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
- }, subagentOptions));
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-Xy8yU8wO.mjs.map
4480
+ //# sourceMappingURL=runtime-B_Ig28og.mjs.map