pi2dsh 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analyzer-CsrBeS8r.mjs → analyzer-CEjVQKXA.mjs} +3 -3
- package/dist/{analyzer-CsrBeS8r.mjs.map → analyzer-CEjVQKXA.mjs.map} +1 -1
- package/dist/cli.mjs +7 -44
- package/dist/cli.mjs.map +1 -1
- 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 +2 -15
- 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-Cn5gWemZ.mjs → mcp-config-DHkfjdvX.mjs} +22 -22
- package/dist/mcp-config-DHkfjdvX.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-Bh5k4Kjt.mjs → runtime-DsPaznXJ.mjs} +993 -221
- package/dist/runtime-DsPaznXJ.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 +11 -8
- package/dist/generator-CSOv6wYT.mjs +0 -298
- package/dist/generator-CSOv6wYT.mjs.map +0 -1
- package/dist/mcp-config-Cn5gWemZ.mjs.map +0 -1
- package/dist/pi-coding-agent-Ctw-BhoY.mjs.map +0 -1
- package/dist/runtime-Bh5k4Kjt.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,
|
|
@@ -332,6 +361,8 @@ var PiBridgedAgentSession = class {
|
|
|
332
361
|
#pendingToolCalls = /* @__PURE__ */ new Set();
|
|
333
362
|
#sessionName = "";
|
|
334
363
|
#turns = 0;
|
|
364
|
+
/** Keeps message projections (which await attachment reads) in log order. */
|
|
365
|
+
#messageProjection = Promise.resolve();
|
|
335
366
|
#aborted = false;
|
|
336
367
|
constructor(host, handle, tools) {
|
|
337
368
|
this.#host = host;
|
|
@@ -464,8 +495,9 @@ var PiBridgedAgentSession = class {
|
|
|
464
495
|
}
|
|
465
496
|
});
|
|
466
497
|
}
|
|
467
|
-
|
|
468
|
-
|
|
498
|
+
this.#messageProjection = this.#messageProjection.then(async () => {
|
|
499
|
+
const message = await this.#host.messageFromSessionEvent(event);
|
|
500
|
+
if (message === void 0) return;
|
|
469
501
|
this.messages.push(message);
|
|
470
502
|
emit({
|
|
471
503
|
type: "message_start",
|
|
@@ -475,19 +507,21 @@ var PiBridgedAgentSession = class {
|
|
|
475
507
|
type: "message_end",
|
|
476
508
|
message
|
|
477
509
|
});
|
|
478
|
-
}
|
|
510
|
+
});
|
|
479
511
|
if (type === "turn/end") {
|
|
480
512
|
this.#turns += 1;
|
|
481
513
|
this.#streaming = false;
|
|
482
514
|
this.#pendingToolCalls.clear();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
515
|
+
this.#messageProjection = this.#messageProjection.then(() => {
|
|
516
|
+
emit({
|
|
517
|
+
type: "turn_end",
|
|
518
|
+
turnIndex: Number(event.data.turn ?? 1) - 1,
|
|
519
|
+
message: {
|
|
520
|
+
role: "assistant",
|
|
521
|
+
content: []
|
|
522
|
+
},
|
|
523
|
+
toolResults: []
|
|
524
|
+
});
|
|
491
525
|
});
|
|
492
526
|
}
|
|
493
527
|
}
|
|
@@ -541,6 +575,7 @@ var PiBridgedAgentSession = class {
|
|
|
541
575
|
}
|
|
542
576
|
}), "followup");
|
|
543
577
|
await completed;
|
|
578
|
+
await this.#messageProjection;
|
|
544
579
|
}
|
|
545
580
|
steer(text) {
|
|
546
581
|
this.#host.piContentToDsh([{
|
|
@@ -663,6 +698,184 @@ async function createBridgedAgentSession(host, options) {
|
|
|
663
698
|
return { session: new PiBridgedAgentSession(host, handle, tools) };
|
|
664
699
|
}
|
|
665
700
|
//#endregion
|
|
701
|
+
//#region src/compat/vendor/pi-tool-validation.ts
|
|
702
|
+
const validatorCache = /* @__PURE__ */ new WeakMap();
|
|
703
|
+
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
|
704
|
+
function getSchemaTypes(schema) {
|
|
705
|
+
if (typeof schema.type === "string") return [schema.type];
|
|
706
|
+
if (Array.isArray(schema.type)) return schema.type.filter((type) => typeof type === "string");
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
function matchesJsonType(value, type) {
|
|
710
|
+
switch (type) {
|
|
711
|
+
case "number": return typeof value === "number";
|
|
712
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
713
|
+
case "boolean": return typeof value === "boolean";
|
|
714
|
+
case "string": return typeof value === "string";
|
|
715
|
+
case "null": return value === null;
|
|
716
|
+
case "array": return Array.isArray(value);
|
|
717
|
+
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
718
|
+
default: return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function getSubSchemaValidator(schema) {
|
|
722
|
+
try {
|
|
723
|
+
return getValidator(schema);
|
|
724
|
+
} catch {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function coercePrimitiveByType(value, type) {
|
|
729
|
+
switch (type) {
|
|
730
|
+
case "number":
|
|
731
|
+
if (value === null) return 0;
|
|
732
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
733
|
+
const parsed = Number(value);
|
|
734
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
735
|
+
}
|
|
736
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
737
|
+
return value;
|
|
738
|
+
case "integer":
|
|
739
|
+
if (value === null) return 0;
|
|
740
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
741
|
+
const parsed = Number(value);
|
|
742
|
+
if (Number.isInteger(parsed)) return parsed;
|
|
743
|
+
}
|
|
744
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
745
|
+
return value;
|
|
746
|
+
case "boolean":
|
|
747
|
+
if (value === null) return false;
|
|
748
|
+
if (typeof value === "string") {
|
|
749
|
+
if (value === "true") return true;
|
|
750
|
+
if (value === "false") return false;
|
|
751
|
+
}
|
|
752
|
+
if (typeof value === "number") {
|
|
753
|
+
if (value === 1) return true;
|
|
754
|
+
if (value === 0) return false;
|
|
755
|
+
}
|
|
756
|
+
return value;
|
|
757
|
+
case "string":
|
|
758
|
+
if (value === null) return "";
|
|
759
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
760
|
+
return value;
|
|
761
|
+
case "null":
|
|
762
|
+
if (value === "" || value === 0 || value === false) return null;
|
|
763
|
+
return value;
|
|
764
|
+
default: return value;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function applySchemaObjectCoercion(value, schema) {
|
|
768
|
+
const properties = schema.properties;
|
|
769
|
+
const definedKeys = new Set(properties ? Object.keys(properties) : []);
|
|
770
|
+
if (properties) for (const [key, propertySchema] of Object.entries(properties)) {
|
|
771
|
+
if (!(key in value)) continue;
|
|
772
|
+
value[key] = coerceWithJsonSchema(value[key], propertySchema);
|
|
773
|
+
}
|
|
774
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") for (const [key, propertyValue] of Object.entries(value)) {
|
|
775
|
+
if (definedKeys.has(key)) continue;
|
|
776
|
+
value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function applySchemaArrayCoercion(value, schema) {
|
|
780
|
+
if (Array.isArray(schema.items)) {
|
|
781
|
+
for (let index = 0; index < value.length; index++) {
|
|
782
|
+
const itemSchema = schema.items[index];
|
|
783
|
+
if (!itemSchema) continue;
|
|
784
|
+
value[index] = coerceWithJsonSchema(value[index], itemSchema);
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (schema.items && typeof schema.items === "object") for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
|
|
789
|
+
}
|
|
790
|
+
function coerceWithUnionSchema(value, schemas) {
|
|
791
|
+
for (const schema of schemas) if (getSubSchemaValidator(schema)?.Check(value)) return value;
|
|
792
|
+
for (const schema of schemas) {
|
|
793
|
+
const coerced = coerceWithJsonSchema(structuredClone(value), schema);
|
|
794
|
+
if (getSubSchemaValidator(schema)?.Check(coerced)) return coerced;
|
|
795
|
+
}
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
function coerceWithJsonSchema(value, schema) {
|
|
799
|
+
let nextValue = value;
|
|
800
|
+
if (Array.isArray(schema.allOf)) for (const nested of schema.allOf) nextValue = coerceWithJsonSchema(nextValue, nested);
|
|
801
|
+
if (Array.isArray(schema.anyOf)) nextValue = coerceWithUnionSchema(nextValue, schema.anyOf);
|
|
802
|
+
if (Array.isArray(schema.oneOf)) nextValue = coerceWithUnionSchema(nextValue, schema.oneOf);
|
|
803
|
+
const schemaTypes = getSchemaTypes(schema);
|
|
804
|
+
const matchesUnionMember = schemaTypes.length > 1 && schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));
|
|
805
|
+
if (schemaTypes.length > 0 && !matchesUnionMember) for (const schemaType of schemaTypes) {
|
|
806
|
+
const candidate = coercePrimitiveByType(nextValue, schemaType);
|
|
807
|
+
if (candidate !== nextValue) {
|
|
808
|
+
nextValue = candidate;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (schemaTypes.includes("object") && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
|
|
813
|
+
if (schemaTypes.includes("array") && Array.isArray(nextValue)) applySchemaArrayCoercion(nextValue, schema);
|
|
814
|
+
return nextValue;
|
|
815
|
+
}
|
|
816
|
+
function normalizeOptionalNulls(value, schema) {
|
|
817
|
+
if (Array.isArray(value)) {
|
|
818
|
+
if (Array.isArray(schema.items)) for (let index = 0; index < value.length; index++) {
|
|
819
|
+
const itemSchema = schema.items[index];
|
|
820
|
+
if (itemSchema) normalizeOptionalNulls(value[index], itemSchema);
|
|
821
|
+
}
|
|
822
|
+
else if (schema.items) for (const item of value) normalizeOptionalNulls(item, schema.items);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (typeof value !== "object" || value === null || !schema.properties) return;
|
|
826
|
+
const object = value;
|
|
827
|
+
const required = new Set(schema.required ?? []);
|
|
828
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
829
|
+
if (!(key in object)) continue;
|
|
830
|
+
if (object[key] === null && !required.has(key) && typeof propertySchema.$ref !== "string" && getSubSchemaValidator(propertySchema)?.Check(null) === false) delete object[key];
|
|
831
|
+
else normalizeOptionalNulls(object[key], propertySchema);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function getValidator(schema) {
|
|
835
|
+
const key = schema;
|
|
836
|
+
const cached = validatorCache.get(key);
|
|
837
|
+
if (cached) return cached;
|
|
838
|
+
const validator = Compile(schema);
|
|
839
|
+
validatorCache.set(key, validator);
|
|
840
|
+
return validator;
|
|
841
|
+
}
|
|
842
|
+
function formatValidationPath(error) {
|
|
843
|
+
if (error.keyword === "required") {
|
|
844
|
+
const requiredProperty = error.params.requiredProperties?.[0];
|
|
845
|
+
if (requiredProperty) {
|
|
846
|
+
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
|
847
|
+
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return error.instancePath.replace(/^\//, "").replace(/\//g, ".") || "root";
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Validates tool call arguments against the tool's TypeBox schema
|
|
854
|
+
* @param tool The tool definition with TypeBox schema
|
|
855
|
+
* @param toolCall The tool call from the LLM
|
|
856
|
+
* @returns The validated (and potentially coerced) arguments
|
|
857
|
+
* @throws Error with formatted message if validation fails
|
|
858
|
+
*/
|
|
859
|
+
function validateToolArguments(tool, toolCall) {
|
|
860
|
+
const args = structuredClone(toolCall.arguments);
|
|
861
|
+
normalizeOptionalNulls(args, tool.parameters);
|
|
862
|
+
Value.Convert(tool.parameters, args);
|
|
863
|
+
const validator = getValidator(tool.parameters);
|
|
864
|
+
if (!Object.getOwnPropertySymbols(tool.parameters).includes(TYPEBOX_KIND)) {
|
|
865
|
+
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
|
866
|
+
if (coerced !== args) {
|
|
867
|
+
if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) {
|
|
868
|
+
for (const key of Object.keys(args)) delete args[key];
|
|
869
|
+
Object.assign(args, coerced);
|
|
870
|
+
} else return validator.Check(coerced) ? coerced : args;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (validator.Check(args)) return args;
|
|
874
|
+
const errors = validator.Errors(args).map((error) => ` - ${formatValidationPath(error)}: ${error.message}`).join("\n") || "Unknown validation error";
|
|
875
|
+
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
|
|
876
|
+
throw new Error(errorMessage);
|
|
877
|
+
}
|
|
878
|
+
//#endregion
|
|
666
879
|
//#region src/model-bridge.ts
|
|
667
880
|
function llmOf(ctx) {
|
|
668
881
|
try {
|
|
@@ -736,14 +949,30 @@ var ModelCatalog = class {
|
|
|
736
949
|
provider: provider.id
|
|
737
950
|
}));
|
|
738
951
|
} catch {}
|
|
952
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
953
|
+
await Promise.all(next.map(async (model) => {
|
|
954
|
+
const exact = await this.#resolveExact(model.provider, model.id);
|
|
955
|
+
if (exact === void 0) return;
|
|
956
|
+
if (exact.contextWindow !== void 0) model.contextWindow = exact.contextWindow;
|
|
957
|
+
if (exact.maxTokens !== void 0) model.maxTokens = exact.maxTokens;
|
|
958
|
+
model.reasoning = exact.reasoning;
|
|
959
|
+
resolved.set(`${model.provider}${model.id}`, model);
|
|
960
|
+
}));
|
|
739
961
|
this.#models = next;
|
|
962
|
+
this.#resolved = resolved;
|
|
740
963
|
}
|
|
741
964
|
/** Exact-route metadata (context window, reasoning efforts), cached per route. */
|
|
742
965
|
async resolve(provider, modelId) {
|
|
743
|
-
if (this.#llm === void 0) return void 0;
|
|
744
966
|
const key = `${provider}${modelId}`;
|
|
745
967
|
const cached = this.#resolved.get(key);
|
|
746
968
|
if (cached !== void 0) return cached;
|
|
969
|
+
const model = await this.#resolveExact(provider, modelId);
|
|
970
|
+
if (model !== void 0) this.#resolved.set(key, model);
|
|
971
|
+
return model;
|
|
972
|
+
}
|
|
973
|
+
/** One exact resolve, uncached: the two callers own their own caching. */
|
|
974
|
+
async #resolveExact(provider, modelId) {
|
|
975
|
+
if (this.#llm === void 0) return void 0;
|
|
747
976
|
try {
|
|
748
977
|
const info = await this.#llm.resolveModelInfo(provider, modelId);
|
|
749
978
|
const model = piModelFromInfo(info);
|
|
@@ -753,7 +982,6 @@ var ModelCatalog = class {
|
|
|
753
982
|
if (typeof defaultMaxTokens === "number") model.maxTokens = defaultMaxTokens;
|
|
754
983
|
const reasoning = info.reasoning;
|
|
755
984
|
model.reasoning = Array.isArray(reasoning?.efforts) && reasoning.efforts.length > 0;
|
|
756
|
-
this.#resolved.set(key, model);
|
|
757
985
|
return model;
|
|
758
986
|
} catch {
|
|
759
987
|
return;
|
|
@@ -1083,26 +1311,34 @@ function normalizeToolSchemas(tools) {
|
|
|
1083
1311
|
}));
|
|
1084
1312
|
}
|
|
1085
1313
|
/** DSH GenerateOptions parts → the Pi request context a provider.stream expects. */
|
|
1086
|
-
function dshRequestToPiContext(options) {
|
|
1314
|
+
async function dshRequestToPiContext(options, attachments) {
|
|
1087
1315
|
const piMessages = [];
|
|
1316
|
+
const toolNamesById = /* @__PURE__ */ new Map();
|
|
1317
|
+
for (const message of Array.isArray(options.messages) ? options.messages : []) {
|
|
1318
|
+
if (message.role !== "assistant") continue;
|
|
1319
|
+
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);
|
|
1320
|
+
}
|
|
1088
1321
|
for (const message of Array.isArray(options.messages) ? options.messages : []) {
|
|
1089
1322
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
1090
1323
|
const source = message.source;
|
|
1091
1324
|
if (message.role === "user" && source?.kind === "tool") {
|
|
1092
1325
|
const result = content[0];
|
|
1326
|
+
const toolCallId = String(result?.toolCallId ?? "");
|
|
1327
|
+
const toolName = toolNamesById.get(toolCallId);
|
|
1093
1328
|
piMessages.push({
|
|
1094
1329
|
role: "toolResult",
|
|
1095
|
-
toolCallId
|
|
1096
|
-
|
|
1330
|
+
toolCallId,
|
|
1331
|
+
...toolName === void 0 ? {} : { toolName },
|
|
1332
|
+
content: await piContentFromDshBlocks(Array.isArray(result?.content) ? result.content : [], attachments),
|
|
1097
1333
|
isError: result?.isError === true
|
|
1098
1334
|
});
|
|
1099
1335
|
} else if (message.role === "user") piMessages.push({
|
|
1100
1336
|
role: "user",
|
|
1101
|
-
content: piContentFromDshBlocks(content)
|
|
1337
|
+
content: await piContentFromDshBlocks(content, attachments)
|
|
1102
1338
|
});
|
|
1103
1339
|
else if (message.role === "assistant") piMessages.push({
|
|
1104
1340
|
role: "assistant",
|
|
1105
|
-
content: piContentFromDshBlocks(content)
|
|
1341
|
+
content: await piContentFromDshBlocks(content, attachments)
|
|
1106
1342
|
});
|
|
1107
1343
|
}
|
|
1108
1344
|
return {
|
|
@@ -1111,7 +1347,12 @@ function dshRequestToPiContext(options) {
|
|
|
1111
1347
|
...Array.isArray(options.tools) ? { tools: options.tools } : {}
|
|
1112
1348
|
};
|
|
1113
1349
|
}
|
|
1114
|
-
|
|
1350
|
+
/** Whether any block in this content — including inside a tool result — is an image. */
|
|
1351
|
+
function blocksContainImage(blocks) {
|
|
1352
|
+
if (!Array.isArray(blocks)) return false;
|
|
1353
|
+
return blocks.some((block) => block?.type === "image" || block?.type === "tool-result" && blocksContainImage(block.content));
|
|
1354
|
+
}
|
|
1355
|
+
async function piContentFromDshBlocks(blocks, attachments) {
|
|
1115
1356
|
const out = [];
|
|
1116
1357
|
for (const block of blocks) if (block.type === "text") out.push({
|
|
1117
1358
|
type: "text",
|
|
@@ -1134,15 +1375,187 @@ function piContentFromDshBlocks(blocks) {
|
|
|
1134
1375
|
name: String(block.name ?? ""),
|
|
1135
1376
|
arguments: parsed
|
|
1136
1377
|
});
|
|
1378
|
+
} else if (block.type === "image") {
|
|
1379
|
+
if (attachments === void 0) throw new LlmError("pi2dsh: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1380
|
+
const stored = await attachments.readImage(block.attachment);
|
|
1381
|
+
out.push({
|
|
1382
|
+
type: "image",
|
|
1383
|
+
data: Buffer.from(stored.data).toString("base64"),
|
|
1384
|
+
mimeType: String(stored.ref?.mediaType ?? block.attachment?.mediaType ?? "image/png")
|
|
1385
|
+
});
|
|
1137
1386
|
}
|
|
1138
1387
|
return out;
|
|
1139
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Classify a failure into the code DSH keys retries and error surfaces off.
|
|
1391
|
+
*
|
|
1392
|
+
* Mirrored from `classifyPiAiError` in DSH's own pi-ai adapter
|
|
1393
|
+
* (`packages/llm/llm-pi-ai/src/stream.ts`). That symbol is private to a
|
|
1394
|
+
* package whose published tarball ships only `lib/`, so it cannot be imported
|
|
1395
|
+
* — but it must not be diverged from either. `RATE_LIMIT`, `SERVER`,
|
|
1396
|
+
* `TIMEOUT` and `TRANSPORT` are four of the five codes in DSH's default
|
|
1397
|
+
* retryable set, so a code invented here would make every rate limit, every
|
|
1398
|
+
* dropped socket and every timeout a Pi provider ever reports permanently
|
|
1399
|
+
* non-retryable — dsh-llm-retry would simply not recognize it. The quota and
|
|
1400
|
+
* context-window predicates it defers to are DSH's own, imported above.
|
|
1401
|
+
* @param message - the provider's failure text.
|
|
1402
|
+
* @returns the harness failure code.
|
|
1403
|
+
*/
|
|
1404
|
+
function classifyPiFailure(message) {
|
|
1405
|
+
if (/\b(?:401|403)\b/.test(message)) return "AUTH";
|
|
1406
|
+
if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
|
|
1407
|
+
if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
|
|
1408
|
+
if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
|
|
1409
|
+
if (/\b5\d\d\b/.test(message)) return "SERVER";
|
|
1410
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(message)) return "TIMEOUT";
|
|
1411
|
+
if (/stream ended (?:before|without)\b/i.test(message)) return "TRANSPORT";
|
|
1412
|
+
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";
|
|
1413
|
+
return "PI_AI_ERROR";
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Pi usage counts → DSH's TokenUsage, mirroring the official adapter's
|
|
1417
|
+
* `mapUsage`: Pi reports zeros rather than absence, so the cache fields appear
|
|
1418
|
+
* only when non-zero.
|
|
1419
|
+
* @param usage - the usage record on a terminal Pi event, if the package sent one.
|
|
1420
|
+
* @returns the harness counts.
|
|
1421
|
+
*/
|
|
1422
|
+
function piUsageToDsh(usage) {
|
|
1423
|
+
const cacheRead = Number(usage?.cacheRead ?? 0);
|
|
1424
|
+
const cacheWrite = Number(usage?.cacheWrite ?? 0);
|
|
1425
|
+
return {
|
|
1426
|
+
inputTokens: Number(usage?.input ?? 0),
|
|
1427
|
+
outputTokens: Number(usage?.output ?? 0),
|
|
1428
|
+
...cacheRead > 0 ? { cacheReadTokens: cacheRead } : {},
|
|
1429
|
+
...cacheWrite > 0 ? { cacheWriteTokens: cacheWrite } : {}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* The assistant message Pi's terminal `error` event carries.
|
|
1434
|
+
*
|
|
1435
|
+
* Pi delivers failures in-stream as `{ type: 'error', reason, error }` where
|
|
1436
|
+
* `error` is an **AssistantMessage**, not an Error — its text lives on
|
|
1437
|
+
* `errorMessage`. A package that hands back a bare Error instead is met
|
|
1438
|
+
* halfway rather than stringified into "[object Object]".
|
|
1439
|
+
* @param error - the event's payload.
|
|
1440
|
+
* @param fallbackStopReason - the event's own `reason`, used when the payload states none.
|
|
1441
|
+
* @returns a message shape the finish-reason mapping can read.
|
|
1442
|
+
*/
|
|
1443
|
+
function piErrorPayload(error, fallbackStopReason) {
|
|
1444
|
+
if (error instanceof Error) return {
|
|
1445
|
+
stopReason: fallbackStopReason,
|
|
1446
|
+
errorMessage: error.message
|
|
1447
|
+
};
|
|
1448
|
+
if (typeof error === "object" && error !== null) {
|
|
1449
|
+
const record = error;
|
|
1450
|
+
if (typeof record.stopReason === "string" || typeof record.errorMessage === "string") return {
|
|
1451
|
+
stopReason: fallbackStopReason,
|
|
1452
|
+
...record
|
|
1453
|
+
};
|
|
1454
|
+
return {
|
|
1455
|
+
stopReason: fallbackStopReason,
|
|
1456
|
+
errorMessage: JSON.stringify(error)
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
stopReason: fallbackStopReason,
|
|
1461
|
+
errorMessage: String(error ?? "pi2dsh: the package reported a stream failure with no detail")
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* A terminal Pi assistant message → DSH's finish reason, mirroring the
|
|
1466
|
+
* official adapter's `mapStopReason`, including its context-overflow and
|
|
1467
|
+
* empty-response detection (the overflow predicate is Pi's own, vendored).
|
|
1468
|
+
*
|
|
1469
|
+
* One documented deviation: DSH's adapter can trust pi-ai to populate
|
|
1470
|
+
* `content`, and reads an empty array as a degenerate empty completion. This
|
|
1471
|
+
* bridge takes its events from an arbitrary third-party package, so it also
|
|
1472
|
+
* accepts the blocks it actually saw stream past as proof of content. That
|
|
1473
|
+
* only ever narrows the error — a stream that produced output is never
|
|
1474
|
+
* mislabelled EMPTY_RESPONSE because its terminal event omitted a field.
|
|
1475
|
+
* @param message - the assistant message on the `done` or `error` event.
|
|
1476
|
+
* @param contextWindow - the model's resolved capacity, for usage-based overflow detection.
|
|
1477
|
+
* @param sawContent - whether any content block streamed before the terminal event.
|
|
1478
|
+
* @returns the harness finish reason.
|
|
1479
|
+
*/
|
|
1480
|
+
function piFinishReason(message, contextWindow, sawContent) {
|
|
1481
|
+
const stopReason = String(message?.stopReason ?? "stop");
|
|
1482
|
+
const errorMessage = typeof message?.errorMessage === "string" ? message.errorMessage : void 0;
|
|
1483
|
+
const model = String(message?.model ?? "unknown");
|
|
1484
|
+
const usage = message?.usage;
|
|
1485
|
+
const probe = {
|
|
1486
|
+
stopReason,
|
|
1487
|
+
errorMessage,
|
|
1488
|
+
usage: {
|
|
1489
|
+
input: Number(usage?.input ?? 0),
|
|
1490
|
+
output: Number(usage?.output ?? 0),
|
|
1491
|
+
cacheRead: Number(usage?.cacheRead ?? 0),
|
|
1492
|
+
cacheWrite: Number(usage?.cacheWrite ?? 0)
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
if (isContextOverflow(probe, contextWindow) || stopReason === "error" && errorMessage !== void 0 && isContextWindowExceededError(errorMessage)) return {
|
|
1496
|
+
kind: "error",
|
|
1497
|
+
failure: {
|
|
1498
|
+
message: errorMessage ?? `pi2dsh: the package's provider reported context overflow for model "${model}"`,
|
|
1499
|
+
code: CONTEXT_WINDOW_EXCEEDED_CODE
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
switch (stopReason) {
|
|
1503
|
+
case "length": return { kind: "max-tokens" };
|
|
1504
|
+
case "toolUse": return { kind: "tool-calls" };
|
|
1505
|
+
case "aborted": return {
|
|
1506
|
+
kind: "aborted",
|
|
1507
|
+
failure: {
|
|
1508
|
+
message: errorMessage ?? "pi2dsh: the package aborted the stream",
|
|
1509
|
+
code: "ABORTED"
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
case "error": {
|
|
1513
|
+
const text = errorMessage ?? "pi2dsh: the package reported a stream failure";
|
|
1514
|
+
return {
|
|
1515
|
+
kind: "error",
|
|
1516
|
+
failure: {
|
|
1517
|
+
message: text,
|
|
1518
|
+
code: classifyPiFailure(text)
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
default:
|
|
1523
|
+
if ((Array.isArray(message?.content) ? message.content : []).length === 0 && !sawContent) return {
|
|
1524
|
+
kind: "error",
|
|
1525
|
+
failure: {
|
|
1526
|
+
message: `model "${model}" returned a completed response with no content`,
|
|
1527
|
+
code: EMPTY_RESPONSE_CODE
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
return { kind: "stop" };
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* The id and name of the tool call an event is opening or extending. Pi states
|
|
1535
|
+
* them on the partial message's content at the event's index, never on the
|
|
1536
|
+
* event itself.
|
|
1537
|
+
* @param event - a `toolcall_start` or `toolcall_delta` event.
|
|
1538
|
+
* @param index - the event's content index.
|
|
1539
|
+
* @returns the call's identity, or undefined when the partial states none.
|
|
1540
|
+
*/
|
|
1541
|
+
function partialToolCall(event, index) {
|
|
1542
|
+
const partial = event.partial;
|
|
1543
|
+
const block = (Array.isArray(partial?.content) ? partial.content : [])[index];
|
|
1544
|
+
if (block === void 0 || block.type !== "toolCall") return void 0;
|
|
1545
|
+
const name = typeof block.name === "string" && block.name.length > 0 ? block.name : void 0;
|
|
1546
|
+
return {
|
|
1547
|
+
id: typeof block.id === "string" ? block.id : "",
|
|
1548
|
+
...name === void 0 ? {} : { name }
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1140
1551
|
/** A Pi AssistantMessageEventStream → DSH StreamChunk sequence. */
|
|
1141
|
-
async function* piEventsToDshChunks(events) {
|
|
1142
|
-
|
|
1552
|
+
async function* piEventsToDshChunks(events, contextWindow) {
|
|
1553
|
+
const openToolCalls = /* @__PURE__ */ new Map();
|
|
1554
|
+
let sawContent = false;
|
|
1143
1555
|
for await (const event of events) {
|
|
1144
1556
|
const type = String(event.type ?? "");
|
|
1145
1557
|
const index = Number(event.contentIndex ?? 0);
|
|
1558
|
+
if (type === "text_start" || type === "thinking_start" || type === "toolcall_start") sawContent = true;
|
|
1146
1559
|
if (type === "text_start") yield {
|
|
1147
1560
|
type: "block-start",
|
|
1148
1561
|
index,
|
|
@@ -1179,18 +1592,24 @@ async function* piEventsToDshChunks(events) {
|
|
|
1179
1592
|
text: String(event.content ?? "")
|
|
1180
1593
|
}
|
|
1181
1594
|
};
|
|
1182
|
-
else if (type === "toolcall_start")
|
|
1183
|
-
|
|
1184
|
-
index,
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1595
|
+
else if (type === "toolcall_start") {
|
|
1596
|
+
const opened = partialToolCall(event, index);
|
|
1597
|
+
if (opened !== void 0) openToolCalls.set(index, opened);
|
|
1598
|
+
yield {
|
|
1599
|
+
type: "block-start",
|
|
1600
|
+
index,
|
|
1601
|
+
blockType: "tool-call"
|
|
1602
|
+
};
|
|
1603
|
+
} else if (type === "toolcall_delta") {
|
|
1604
|
+
const open = openToolCalls.get(index) ?? partialToolCall(event, index);
|
|
1605
|
+
yield {
|
|
1606
|
+
type: "tool-call-delta",
|
|
1607
|
+
index,
|
|
1608
|
+
id: open?.id ?? "",
|
|
1609
|
+
...open?.name === void 0 ? {} : { name: open.name },
|
|
1610
|
+
argumentsDelta: String(event.delta ?? "")
|
|
1611
|
+
};
|
|
1612
|
+
} else if (type === "toolcall_end") {
|
|
1194
1613
|
const call = event.toolCall;
|
|
1195
1614
|
yield {
|
|
1196
1615
|
type: "block-end",
|
|
@@ -1204,61 +1623,34 @@ async function* piEventsToDshChunks(events) {
|
|
|
1204
1623
|
};
|
|
1205
1624
|
} else if (type === "done") {
|
|
1206
1625
|
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");
|
|
1626
|
+
yield {
|
|
1627
|
+
type: "usage",
|
|
1628
|
+
usage: piUsageToDsh(message?.usage)
|
|
1629
|
+
};
|
|
1221
1630
|
yield {
|
|
1222
1631
|
type: "finish",
|
|
1223
|
-
reason:
|
|
1224
|
-
kind: "aborted",
|
|
1225
|
-
failure: {
|
|
1226
|
-
message: "aborted by caller",
|
|
1227
|
-
code: "ABORTED"
|
|
1228
|
-
}
|
|
1229
|
-
} : { kind: "stop" }
|
|
1632
|
+
reason: piFinishReason(message, contextWindow, sawContent)
|
|
1230
1633
|
};
|
|
1231
1634
|
return;
|
|
1232
1635
|
} else if (type === "error") {
|
|
1233
|
-
const
|
|
1636
|
+
const message = piErrorPayload(event.error, String(event.reason ?? "error"));
|
|
1637
|
+
yield {
|
|
1638
|
+
type: "usage",
|
|
1639
|
+
usage: piUsageToDsh(message.usage)
|
|
1640
|
+
};
|
|
1234
1641
|
yield {
|
|
1235
1642
|
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
|
-
}
|
|
1643
|
+
reason: piFinishReason(message, contextWindow, sawContent)
|
|
1243
1644
|
};
|
|
1244
1645
|
return;
|
|
1245
1646
|
}
|
|
1246
1647
|
}
|
|
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
|
-
};
|
|
1648
|
+
throw new LlmError("pi2dsh: the package's provider stream ended without a done or error event", "STREAM_CLOSED");
|
|
1257
1649
|
}
|
|
1258
1650
|
//#endregion
|
|
1259
1651
|
//#region src/provider-adapter.ts
|
|
1260
1652
|
function providerCarriesTransport(value) {
|
|
1261
|
-
return typeof value?.stream === "function";
|
|
1653
|
+
return typeof value?.streamSimple === "function" || typeof value?.stream === "function";
|
|
1262
1654
|
}
|
|
1263
1655
|
function providerModels(provider) {
|
|
1264
1656
|
try {
|
|
@@ -1280,6 +1672,19 @@ const PI_CARRIED_FIELDS = [
|
|
|
1280
1672
|
"headers"
|
|
1281
1673
|
];
|
|
1282
1674
|
/**
|
|
1675
|
+
* Model capabilities DSH asks about by its own names. Shared by the catalog
|
|
1676
|
+
* listing and the exact-route resolve deliberately: a capability present in
|
|
1677
|
+
* one and absent from the other is how a model silently loses an ability —
|
|
1678
|
+
* the host consults the resolve before a request, so a modality declared only
|
|
1679
|
+
* in the listing reads as "text only" at the moment it matters.
|
|
1680
|
+
* @param model - the Pi model descriptor as its package declared it.
|
|
1681
|
+
*/
|
|
1682
|
+
function capabilityProjection(model) {
|
|
1683
|
+
if (!Array.isArray(model.input)) return {};
|
|
1684
|
+
const modalities = model.input.filter((entry) => entry === "text" || entry === "image");
|
|
1685
|
+
return modalities.length === 0 ? {} : { inputModalities: modalities };
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1283
1688
|
* Pi's reasoning capability, in DSH's shape.
|
|
1284
1689
|
*
|
|
1285
1690
|
* The two sides say the same thing differently: Pi carries a `reasoning`
|
|
@@ -1316,6 +1721,12 @@ function piCarriedFields(model) {
|
|
|
1316
1721
|
* Structural typing carries it through `llm.registerAdapter`.
|
|
1317
1722
|
*/
|
|
1318
1723
|
function piProviderDshAdapter(providerId, provider, host) {
|
|
1724
|
+
let warnedStop = false;
|
|
1725
|
+
const warnStopUnsupported = () => {
|
|
1726
|
+
if (warnedStop) return;
|
|
1727
|
+
warnedStop = true;
|
|
1728
|
+
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`);
|
|
1729
|
+
};
|
|
1319
1730
|
return {
|
|
1320
1731
|
providerInfo: (id) => ({
|
|
1321
1732
|
id,
|
|
@@ -1324,15 +1735,16 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1324
1735
|
providerRetryPolicy: () => void 0,
|
|
1325
1736
|
listModels: async (id) => providerModels(provider).map((model) => ({
|
|
1326
1737
|
...piCarriedFields(model),
|
|
1738
|
+
...capabilityProjection(model),
|
|
1327
1739
|
provider: id,
|
|
1328
1740
|
id: String(model.id ?? ""),
|
|
1329
|
-
name: String(model.name ?? model.id ?? "")
|
|
1330
|
-
...Array.isArray(model.input) ? { inputModalities: model.input.filter((m) => m === "text" || m === "image") } : {}
|
|
1741
|
+
name: String(model.name ?? model.id ?? "")
|
|
1331
1742
|
})),
|
|
1332
1743
|
resolveModel: async (id, modelId) => {
|
|
1333
1744
|
const known = providerModels(provider).find((model) => model.id === modelId);
|
|
1334
1745
|
return {
|
|
1335
1746
|
...known === void 0 ? {} : piCarriedFields(known),
|
|
1747
|
+
...known === void 0 ? {} : capabilityProjection(known),
|
|
1336
1748
|
...known === void 0 ? {} : reasoningProjection(known),
|
|
1337
1749
|
provider: id,
|
|
1338
1750
|
id: modelId,
|
|
@@ -1348,7 +1760,14 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1348
1760
|
id: modelId,
|
|
1349
1761
|
provider: providerId
|
|
1350
1762
|
};
|
|
1351
|
-
const
|
|
1763
|
+
const carriesImage = (Array.isArray(options.messages) ? options.messages : []).some((message) => blocksContainImage(message.content));
|
|
1764
|
+
if (carriesImage) {
|
|
1765
|
+
if (!(Array.isArray(model.input) ? model.input : []).includes("image")) throw new LlmError(`pi2dsh: model "${modelId}" does not declare image input`, "UNSUPPORTED_CONTENT");
|
|
1766
|
+
}
|
|
1767
|
+
const attachments = carriesImage ? host.resolveAttachments?.() : void 0;
|
|
1768
|
+
if (carriesImage && attachments === void 0) throw new LlmError("pi2dsh: image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1769
|
+
const piContext = await dshRequestToPiContext(options, attachments);
|
|
1770
|
+
const effort = typeof options.reasoningEffort === "string" ? options.reasoningEffort : void 0;
|
|
1352
1771
|
const piOptions = {
|
|
1353
1772
|
...auth?.apiKey === void 0 ? {} : { apiKey: auth.apiKey },
|
|
1354
1773
|
...auth?.baseUrl === void 0 ? {} : { baseUrl: auth.baseUrl },
|
|
@@ -1356,9 +1775,20 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1356
1775
|
...options.signal instanceof AbortSignal ? { signal: options.signal } : {},
|
|
1357
1776
|
...typeof options.maxTokens === "number" ? { maxTokens: options.maxTokens } : {},
|
|
1358
1777
|
...typeof options.temperature === "number" ? { temperature: options.temperature } : {},
|
|
1359
|
-
...typeof options.
|
|
1778
|
+
...typeof options.sessionId === "string" ? { sessionId: options.sessionId } : {},
|
|
1779
|
+
...effort === void 0 || effort === "off" ? {} : { reasoning: effort }
|
|
1360
1780
|
};
|
|
1361
|
-
|
|
1781
|
+
const contextWindow = typeof model.contextWindow === "number" ? model.contextWindow : void 0;
|
|
1782
|
+
if (Array.isArray(options.stop) && options.stop.length > 0) warnStopUnsupported();
|
|
1783
|
+
if (typeof provider.streamSimple === "function") {
|
|
1784
|
+
yield* piEventsToDshChunks(provider.streamSimple(model, piContext, piOptions), contextWindow);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const { reasoning, ...rest } = piOptions;
|
|
1788
|
+
yield* piEventsToDshChunks(provider.stream(model, piContext, reasoning === void 0 ? rest : {
|
|
1789
|
+
...rest,
|
|
1790
|
+
reasoningEffort: reasoning
|
|
1791
|
+
}), contextWindow);
|
|
1362
1792
|
}
|
|
1363
1793
|
};
|
|
1364
1794
|
}
|
|
@@ -1772,11 +2202,12 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
1772
2202
|
const notices = [];
|
|
1773
2203
|
const userQuestions = optionalService(ctx, "userQuestions");
|
|
1774
2204
|
const ui = {
|
|
1775
|
-
notify(message) {
|
|
2205
|
+
notify(message, type) {
|
|
1776
2206
|
const text = String(message);
|
|
1777
2207
|
notices.push(text);
|
|
1778
2208
|
state.notifications.push(text);
|
|
1779
|
-
|
|
2209
|
+
const level = type === "warning" || type === "error" ? "warn" : "info";
|
|
2210
|
+
logger(ctx)[level](`[pi2dsh] ${text}`);
|
|
1780
2211
|
},
|
|
1781
2212
|
select: (title, options) => askOne(ctx, agent, signal, {
|
|
1782
2213
|
id: "pi2dsh-select",
|
|
@@ -1989,9 +2420,18 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
1989
2420
|
};
|
|
1990
2421
|
const contextCwd = typeof (sessionOverride?.header)?.cwd === "string" ? sessionOverride.header.cwd : cwdOf(agent);
|
|
1991
2422
|
const base = {
|
|
2423
|
+
...sessionOverride === void 0 ? {} : {
|
|
2424
|
+
sendMessage: (message, options = {}) => sendPiMessage(ctx, state, message.content, deliveryMode(options), sessionOverride, typeof message.customType === "string" ? message.customType : void 0),
|
|
2425
|
+
sendUserMessage: (content) => sendPiMessage(ctx, state, content, "followup", sessionOverride),
|
|
2426
|
+
appendEntry: (customType, data) => {
|
|
2427
|
+
state.bridge.appendCustomEntry(String(sessionOverride.id ?? ""), customType, data);
|
|
2428
|
+
}
|
|
2429
|
+
},
|
|
1992
2430
|
ui,
|
|
1993
2431
|
mode: "rpc",
|
|
1994
|
-
hasUI
|
|
2432
|
+
get hasUI() {
|
|
2433
|
+
return humanAnswererAvailable(userQuestions, agent);
|
|
2434
|
+
},
|
|
1995
2435
|
cwd: contextCwd,
|
|
1996
2436
|
sessionManager: session === void 0 ? state.bridge.readonlySessionManager({
|
|
1997
2437
|
id: "pi2dsh-detached",
|
|
@@ -1999,7 +2439,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
1999
2439
|
}, contextCwd) : state.bridge.readonlySessionManager(session, contextCwd),
|
|
2000
2440
|
modelRegistry,
|
|
2001
2441
|
model: agent === void 0 ? void 0 : currentPiModel(state, agent),
|
|
2002
|
-
scopedModels:
|
|
2442
|
+
scopedModels: [],
|
|
2003
2443
|
thinkingLevel: thinkingLevelOf(state, agent),
|
|
2004
2444
|
isIdle: () => command,
|
|
2005
2445
|
isProjectTrusted: () => false,
|
|
@@ -2012,7 +2452,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2012
2452
|
reason: "pi2dsh: aborted by migrated Pi extension"
|
|
2013
2453
|
});
|
|
2014
2454
|
},
|
|
2015
|
-
hasPendingMessages: () =>
|
|
2455
|
+
hasPendingMessages: () => agent?.inbox?.hasPending === true,
|
|
2016
2456
|
shutdown: () => {
|
|
2017
2457
|
capabilityLedgerOf(ctx, state).reportHostDecision({
|
|
2018
2458
|
capability: "ctx.shutdown",
|
|
@@ -2214,39 +2654,93 @@ async function dispatch(state, eventName, event, eventContext) {
|
|
|
2214
2654
|
for (const handler of state.handlers.get(eventName) ?? []) results.push(await state.agentScope.run(agent, () => handler(event, eventContext)));
|
|
2215
2655
|
return results;
|
|
2216
2656
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2657
|
+
/**
|
|
2658
|
+
* What triggered a compaction, in Pi's vocabulary.
|
|
2659
|
+
*
|
|
2660
|
+
* DSH's durable lifecycle events say two things about the trigger: a manual
|
|
2661
|
+
* compaction runs with no open turn (`turn: null`), and one a command drove
|
|
2662
|
+
* cites that command. Both are Pi's "manual".
|
|
2663
|
+
*
|
|
2664
|
+
* The limit, stated rather than papered over: DSH's automatic trigger is
|
|
2665
|
+
* `'pressure' | 'context-overflow'` at the call site but is NOT written to the
|
|
2666
|
+
* log, so Pi's `threshold` and `overflow` cannot be told apart after the fact.
|
|
2667
|
+
* Automatic compactions report `threshold`, the far more common of the two —
|
|
2668
|
+
* and `willRetry` stays false for the same reason. A package keying behavior
|
|
2669
|
+
* on `overflow` specifically will not see it.
|
|
2670
|
+
* @param data - the `compaction/start` or `compaction/summary` event data.
|
|
2671
|
+
*/
|
|
2672
|
+
function compactionReason(data) {
|
|
2673
|
+
if (data.sourceCommandId !== void 0) return "manual";
|
|
2674
|
+
return data.turn === null ? "manual" : "threshold";
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* DSH content → the Pi content blocks packages read.
|
|
2678
|
+
*
|
|
2679
|
+
* Asynchronous because Pi's image block carries the bytes inline while DSH's
|
|
2680
|
+
* carries only an attachment reference, and the bytes come from the attachment
|
|
2681
|
+
* service. The sync version this replaced projected an image as a bare
|
|
2682
|
+
* `{type:'image'}` — a block that announces an image and contains none, which
|
|
2683
|
+
* is the exact shape a package cannot tell from a real one.
|
|
2684
|
+
* @param ctx - context used to reach the attachment service.
|
|
2685
|
+
* @param content - the DSH blocks to project.
|
|
2686
|
+
*/
|
|
2687
|
+
async function dshToPiContent(ctx, content) {
|
|
2688
|
+
const out = [];
|
|
2689
|
+
for (const block of content) if (block.type === "text") out.push({
|
|
2690
|
+
type: "text",
|
|
2691
|
+
text: block.text
|
|
2692
|
+
});
|
|
2693
|
+
else if (block.type === "reasoning") out.push({
|
|
2694
|
+
type: "thinking",
|
|
2695
|
+
thinking: block.text
|
|
2696
|
+
});
|
|
2697
|
+
else if (block.type === "tool-call") out.push({
|
|
2698
|
+
type: "toolCall",
|
|
2699
|
+
id: block.id,
|
|
2700
|
+
name: block.name,
|
|
2701
|
+
arguments: block.arguments
|
|
2234
2702
|
});
|
|
2703
|
+
else if (block.type === "image") {
|
|
2704
|
+
const image = await piImageBlock(ctx, block.attachment);
|
|
2705
|
+
if (image !== void 0) out.push(image);
|
|
2706
|
+
} else out.push({ type: block.type });
|
|
2707
|
+
return out;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* One DSH image attachment as Pi's inline image block.
|
|
2711
|
+
* @param ctx - context used to reach the attachment service.
|
|
2712
|
+
* @param attachment - the durable reference on an image block.
|
|
2713
|
+
* @returns the Pi block, or undefined when the bytes cannot be read.
|
|
2714
|
+
*/
|
|
2715
|
+
async function piImageBlock(ctx, attachment) {
|
|
2716
|
+
if (attachment === void 0 || attachment === null) return void 0;
|
|
2717
|
+
const attachments = optionalService(ctx, "attachments");
|
|
2718
|
+
if (attachments === void 0) return void 0;
|
|
2719
|
+
try {
|
|
2720
|
+
const stored = await attachments.readImage(attachment);
|
|
2721
|
+
return {
|
|
2722
|
+
type: "image",
|
|
2723
|
+
data: Buffer.from(stored.data).toString("base64"),
|
|
2724
|
+
mimeType: String(attachment.mediaType ?? "image/png")
|
|
2725
|
+
};
|
|
2726
|
+
} catch {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2235
2729
|
}
|
|
2236
|
-
function messageFromSessionEvent(event) {
|
|
2730
|
+
async function messageFromSessionEvent(ctx, event) {
|
|
2237
2731
|
const type = event.type;
|
|
2238
2732
|
const data = event.data;
|
|
2239
2733
|
if (typeof data !== "object" || data === null) return void 0;
|
|
2240
2734
|
const record = data;
|
|
2241
2735
|
if (type === "user/message") return {
|
|
2242
2736
|
role: "user",
|
|
2243
|
-
content: dshToPiContent(record.content ?? [])
|
|
2737
|
+
content: await dshToPiContent(ctx, record.content ?? [])
|
|
2244
2738
|
};
|
|
2245
2739
|
if (type === "assistant/message") {
|
|
2246
2740
|
const message = record.message;
|
|
2247
2741
|
return {
|
|
2248
2742
|
role: "assistant",
|
|
2249
|
-
content: dshToPiContent(message?.content ?? [])
|
|
2743
|
+
content: await dshToPiContent(ctx, message?.content ?? [])
|
|
2250
2744
|
};
|
|
2251
2745
|
}
|
|
2252
2746
|
if (type === "tool/result") {
|
|
@@ -2254,7 +2748,7 @@ function messageFromSessionEvent(event) {
|
|
|
2254
2748
|
return {
|
|
2255
2749
|
role: "toolResult",
|
|
2256
2750
|
toolCallId: tool?.toolCallId,
|
|
2257
|
-
content: dshToPiContent(tool?.content ?? []),
|
|
2751
|
+
content: await dshToPiContent(ctx, tool?.content ?? []),
|
|
2258
2752
|
isError: tool?.isError === true
|
|
2259
2753
|
};
|
|
2260
2754
|
}
|
|
@@ -2301,13 +2795,42 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2301
2795
|
const eventContext = contextFor(ctx, state, agent, void 0);
|
|
2302
2796
|
const type = event.type;
|
|
2303
2797
|
if (type === "turn/start") {
|
|
2304
|
-
|
|
2798
|
+
if (session !== void 0) state.piTurnIndex.set(session, 0);
|
|
2305
2799
|
dispatch(state, "agent_start", { type: "agent_start" }, eventContext).catch((error) => warn("agent_start", error));
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2800
|
+
}
|
|
2801
|
+
if (type === "step/start") dispatch(state, "turn_start", {
|
|
2802
|
+
type: "turn_start",
|
|
2803
|
+
turnIndex: state.piTurnIndex.get(session) ?? 0,
|
|
2804
|
+
timestamp: event.time ?? Date.now()
|
|
2805
|
+
}, eventContext).catch((error) => warn("turn_start", error));
|
|
2806
|
+
if (type === "step/end") {
|
|
2807
|
+
const data = event.data;
|
|
2808
|
+
const turn = Number(data.turn ?? 1);
|
|
2809
|
+
const step = Number(data.step ?? 1);
|
|
2810
|
+
const key = session;
|
|
2811
|
+
const index = state.piTurnIndex.get(key) ?? 0;
|
|
2812
|
+
state.piTurnIndex.set(key, index + 1);
|
|
2813
|
+
state.projection = state.projection.then(async () => {
|
|
2814
|
+
const stepEvents = (session.events ?? []).filter((entry) => {
|
|
2815
|
+
const entryData = entry.data;
|
|
2816
|
+
return Number(entryData?.turn ?? -1) === turn && Number(entryData?.step ?? -1) === step;
|
|
2817
|
+
});
|
|
2818
|
+
const toolResults = (await Promise.all(stepEvents.filter((entry) => entry.type === "tool/result").map((entry) => messageFromSessionEvent(ctx, entry)))).filter((message) => message !== void 0);
|
|
2819
|
+
const assistant = stepEvents.findLast((entry) => entry.type === "assistant/message");
|
|
2820
|
+
const message = assistant === void 0 ? {
|
|
2821
|
+
role: "assistant",
|
|
2822
|
+
content: []
|
|
2823
|
+
} : await messageFromSessionEvent(ctx, assistant) ?? {
|
|
2824
|
+
role: "assistant",
|
|
2825
|
+
content: []
|
|
2826
|
+
};
|
|
2827
|
+
await dispatch(state, "turn_end", {
|
|
2828
|
+
type: "turn_end",
|
|
2829
|
+
turnIndex: index,
|
|
2830
|
+
message,
|
|
2831
|
+
toolResults
|
|
2832
|
+
}, eventContext);
|
|
2833
|
+
}).catch((error) => warn("turn_end", error));
|
|
2311
2834
|
}
|
|
2312
2835
|
if (type === "tool/call") {
|
|
2313
2836
|
const data = event.data;
|
|
@@ -2358,21 +2881,26 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2358
2881
|
type: "session_before_compact",
|
|
2359
2882
|
preparation: { ...event.data },
|
|
2360
2883
|
branchEntries: [],
|
|
2361
|
-
reason:
|
|
2884
|
+
reason: compactionReason(event.data),
|
|
2362
2885
|
willRetry: false
|
|
2363
2886
|
}, eventContext).catch((error) => warn("session_before_compact", error));
|
|
2364
|
-
if (type === "compaction/
|
|
2887
|
+
if (type === "compaction/summary") {
|
|
2365
2888
|
const data = event.data;
|
|
2889
|
+
const shadowed = data.shadowedRange;
|
|
2890
|
+
const usage = data.usage;
|
|
2366
2891
|
dispatch(state, "session_compact", {
|
|
2367
2892
|
type: "session_compact",
|
|
2368
2893
|
compactionEntry: {
|
|
2369
2894
|
type: "compaction",
|
|
2370
2895
|
id: `dsh-${String(event.seq ?? "")}`,
|
|
2371
|
-
summary:
|
|
2372
|
-
|
|
2896
|
+
summary: textBlocks(data.summary).map((block) => block.text).join("\n"),
|
|
2897
|
+
firstKeptEntryId: typeof shadowed?.end === "number" ? `dsh-${shadowed.end + 1}` : "",
|
|
2898
|
+
tokensBefore: Number(data.shadowedTokenCount ?? 0),
|
|
2899
|
+
...usage === void 0 ? {} : { usage },
|
|
2900
|
+
fromHook: false
|
|
2373
2901
|
},
|
|
2374
2902
|
fromExtension: false,
|
|
2375
|
-
reason:
|
|
2903
|
+
reason: compactionReason(data),
|
|
2376
2904
|
willRetry: false
|
|
2377
2905
|
}, eventContext).catch((error) => warn("session_compact", error));
|
|
2378
2906
|
}
|
|
@@ -2390,29 +2918,25 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2390
2918
|
state.lastLoggedModels.set(agent, String(model));
|
|
2391
2919
|
}
|
|
2392
2920
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
role: "assistant",
|
|
2408
|
-
content: []
|
|
2409
|
-
},
|
|
2410
|
-
toolResults: []
|
|
2411
|
-
}, eventContext).then(() => dispatch(state, "agent_end", {
|
|
2921
|
+
state.projection = state.projection.then(async () => {
|
|
2922
|
+
const message = await messageFromSessionEvent(ctx, event);
|
|
2923
|
+
if (message === void 0) return;
|
|
2924
|
+
await dispatch(state, "message_start", {
|
|
2925
|
+
type: "message_start",
|
|
2926
|
+
message
|
|
2927
|
+
}, eventContext);
|
|
2928
|
+
await dispatch(state, "message_end", {
|
|
2929
|
+
type: "message_end",
|
|
2930
|
+
message
|
|
2931
|
+
}, eventContext);
|
|
2932
|
+
}).catch((error) => warn("message lifecycle", error));
|
|
2933
|
+
if (type === "turn/end") state.projection = state.projection.then(async () => {
|
|
2934
|
+
await dispatch(state, "agent_end", {
|
|
2412
2935
|
type: "agent_end",
|
|
2413
2936
|
messages: []
|
|
2414
|
-
}, eventContext)
|
|
2415
|
-
|
|
2937
|
+
}, eventContext);
|
|
2938
|
+
await dispatch(state, "agent_settled", { type: "agent_settled" }, eventContext);
|
|
2939
|
+
}).catch((error) => warn("turn end lifecycle", error));
|
|
2416
2940
|
});
|
|
2417
2941
|
cordis.on("agent/request", async (payload, next) => {
|
|
2418
2942
|
const config = await next();
|
|
@@ -2428,19 +2952,6 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2428
2952
|
...thinking === void 0 || thinking === "off" ? {} : { reasoningEffort: thinking }
|
|
2429
2953
|
};
|
|
2430
2954
|
});
|
|
2431
|
-
cordis.on("tools/result", (exec, result) => {
|
|
2432
|
-
const agent = exec.agent;
|
|
2433
|
-
dispatch(state, "tool_execution_end", {
|
|
2434
|
-
type: "tool_execution_end",
|
|
2435
|
-
toolCallId: exec.callId,
|
|
2436
|
-
toolName: exec.name,
|
|
2437
|
-
result: {
|
|
2438
|
-
content: dshToPiContent(result.content),
|
|
2439
|
-
details: result.meta ?? null
|
|
2440
|
-
},
|
|
2441
|
-
isError: result.isError
|
|
2442
|
-
}, contextFor(ctx, state, agent, exec.signal)).catch((error) => warn("tool_execution_end", error));
|
|
2443
|
-
});
|
|
2444
2955
|
cordis.effect(() => async () => {
|
|
2445
2956
|
for (const agent of state.activeAgents) if (typeof agent === "object" && agent !== null && !state.disposedAgents.has(agent)) {
|
|
2446
2957
|
state.disposedAgents.add(agent);
|
|
@@ -2458,6 +2969,7 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2458
2969
|
function subscribeInterceptors(ctx, state) {
|
|
2459
2970
|
const cordis = ctx;
|
|
2460
2971
|
cordis.on("tools/pre-execute", async (exec, next) => {
|
|
2972
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2461
2973
|
const input = cloneJson(exec.arguments);
|
|
2462
2974
|
const event = {
|
|
2463
2975
|
type: "tool_call",
|
|
@@ -2473,21 +2985,47 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2473
2985
|
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`
|
|
2474
2986
|
};
|
|
2475
2987
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2988
|
+
const agent = exec.agent;
|
|
2989
|
+
const tally = agent === void 0 ? void 0 : state.terminateBatch.get(agent) ?? {
|
|
2990
|
+
calls: 0,
|
|
2991
|
+
terminating: 0
|
|
2479
2992
|
};
|
|
2993
|
+
if (tally !== void 0 && agent !== void 0) {
|
|
2994
|
+
tally.calls += 1;
|
|
2995
|
+
state.terminateBatch.set(agent, tally);
|
|
2996
|
+
}
|
|
2997
|
+
for (const result of results) {
|
|
2998
|
+
if (typeof result !== "object" || result === null) continue;
|
|
2999
|
+
const record = result;
|
|
3000
|
+
if (record.block !== true) continue;
|
|
3001
|
+
if (record.terminate === true && tally !== void 0) tally.terminating += 1;
|
|
3002
|
+
return {
|
|
3003
|
+
kind: "deny",
|
|
3004
|
+
reason: String(record.reason ?? "blocked by migrated Pi tool_call hook")
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
2480
3007
|
return next();
|
|
2481
3008
|
});
|
|
2482
3009
|
cordis.on("tools/post-execute", async (exec, result, next) => {
|
|
3010
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2483
3011
|
const downstream = await next();
|
|
3012
|
+
await dispatch(state, "tool_execution_end", {
|
|
3013
|
+
type: "tool_execution_end",
|
|
3014
|
+
toolCallId: exec.callId,
|
|
3015
|
+
toolName: exec.name,
|
|
3016
|
+
result: {
|
|
3017
|
+
content: await dshToPiContent(ctx, result.content),
|
|
3018
|
+
details: result.meta ?? null
|
|
3019
|
+
},
|
|
3020
|
+
isError: result.isError
|
|
3021
|
+
}, contextFor(ctx, state, exec.agent, exec.signal)).catch((error) => logger(ctx).warn(`[pi2dsh] tool_execution_end handler failed: ${String(error)}`));
|
|
2484
3022
|
if (downstream.kind === "block") return downstream;
|
|
2485
3023
|
const event = {
|
|
2486
3024
|
type: "tool_result",
|
|
2487
3025
|
toolName: exec.name,
|
|
2488
3026
|
toolCallId: exec.callId,
|
|
2489
3027
|
input: cloneJson(exec.arguments),
|
|
2490
|
-
content: dshToPiContent(result.content),
|
|
3028
|
+
content: await dshToPiContent(ctx, result.content),
|
|
2491
3029
|
details: result.meta ?? null,
|
|
2492
3030
|
isError: result.isError,
|
|
2493
3031
|
usage: void 0
|
|
@@ -2503,57 +3041,37 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2503
3041
|
feedback: content
|
|
2504
3042
|
};
|
|
2505
3043
|
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");
|
|
2506
|
-
if (!jsonEqual(event.content, dshToPiContent(result.content))) return {
|
|
3044
|
+
if (!jsonEqual(event.content, await dshToPiContent(ctx, result.content))) return {
|
|
2507
3045
|
kind: "accept",
|
|
2508
3046
|
content
|
|
2509
3047
|
};
|
|
2510
3048
|
return downstream;
|
|
2511
3049
|
});
|
|
3050
|
+
cordis.on("agent/inbox/claimed", (payload) => {
|
|
3051
|
+
const agent = payload.agent;
|
|
3052
|
+
if (agent === void 0 || isSubagentOrigin(agent)) return;
|
|
3053
|
+
const claimed = state.claimedForStep.get(agent) ?? [];
|
|
3054
|
+
claimed.push({
|
|
3055
|
+
message: payload.message,
|
|
3056
|
+
turn: Number(payload.turn ?? 0)
|
|
3057
|
+
});
|
|
3058
|
+
state.claimedForStep.set(agent, claimed);
|
|
3059
|
+
});
|
|
2512
3060
|
cordis.on("agent/pre-step", async (payload, next) => {
|
|
2513
|
-
const decision = await next();
|
|
2514
|
-
if (decision.kind !== "enter" || payload.step !== 1) return decision;
|
|
2515
|
-
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return decision;
|
|
2516
3061
|
const agent = payload.agent;
|
|
2517
|
-
if (isSubagentOrigin(agent)) return
|
|
3062
|
+
if (isSubagentOrigin(agent)) return next();
|
|
3063
|
+
const tally = state.terminateBatch.get(agent);
|
|
3064
|
+
state.terminateBatch.delete(agent);
|
|
3065
|
+
if (tally !== void 0 && tally.calls > 0 && tally.calls === tally.terminating) {
|
|
3066
|
+
logger(ctx).info(`[pi2dsh] a Pi tool_call hook terminated the turn: all ${tally.calls} call(s) in the batch were blocked with terminate`);
|
|
3067
|
+
return { kind: "reject" };
|
|
3068
|
+
}
|
|
3069
|
+
const decision = await next();
|
|
3070
|
+
if (decision.kind !== "enter") return decision;
|
|
2518
3071
|
const signal = payload.signal;
|
|
2519
3072
|
const messages = decision.messages ?? [];
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
const images = await collectPiImages(ctx, userMessages);
|
|
2523
|
-
state.turnSystemPromptOverrides.delete(agent);
|
|
2524
|
-
const event = {
|
|
2525
|
-
type: "before_agent_start",
|
|
2526
|
-
prompt,
|
|
2527
|
-
...images.length > 0 ? { images } : {},
|
|
2528
|
-
systemPrompt: state.currentSystemPrompt,
|
|
2529
|
-
systemPromptOptions: {}
|
|
2530
|
-
};
|
|
2531
|
-
const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
|
|
2532
|
-
const injected = [];
|
|
2533
|
-
for (const result of results) {
|
|
2534
|
-
if (typeof result !== "object" || result === null) continue;
|
|
2535
|
-
const record = result;
|
|
2536
|
-
if (typeof record.systemPrompt === "string") {
|
|
2537
|
-
state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
|
|
2538
|
-
event.systemPrompt = record.systemPrompt;
|
|
2539
|
-
}
|
|
2540
|
-
const message = record.message;
|
|
2541
|
-
if (message !== void 0) {
|
|
2542
|
-
const content = message.content;
|
|
2543
|
-
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2544
|
-
type: "text",
|
|
2545
|
-
text: content
|
|
2546
|
-
}] : content ?? []);
|
|
2547
|
-
injected.push(createUserMessage({
|
|
2548
|
-
content: blocks,
|
|
2549
|
-
source: {
|
|
2550
|
-
kind: "plugin",
|
|
2551
|
-
plugin: state.messageSource,
|
|
2552
|
-
...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
|
|
2553
|
-
}
|
|
2554
|
-
}));
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
3073
|
+
const injected = state.pendingInjections.get(agent) ?? [];
|
|
3074
|
+
state.pendingInjections.delete(agent);
|
|
2557
3075
|
const stepMessages = injected.length === 0 ? messages : [...messages, ...injected];
|
|
2558
3076
|
const transformed = await applyPiContextTransform(ctx, state, agent, signal, stepMessages);
|
|
2559
3077
|
if (transformed === stepMessages && injected.length === 0) return decision;
|
|
@@ -2562,13 +3080,19 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2562
3080
|
messages: transformed
|
|
2563
3081
|
};
|
|
2564
3082
|
});
|
|
3083
|
+
const systemPrompt = optionalService(ctx, "systemPrompt");
|
|
3084
|
+
if (systemPrompt !== void 0) ctx.effect(() => systemPrompt.section({
|
|
3085
|
+
name: "pi2dsh:tool-guidance",
|
|
3086
|
+
order: 150,
|
|
3087
|
+
text: () => piToolPromptContribution(ctx, state)
|
|
3088
|
+
}));
|
|
2565
3089
|
cordis.on("system-prompt/assemble", async (assembly, assembleContext, next) => {
|
|
2566
3090
|
const downstream = await next();
|
|
2567
|
-
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2568
3091
|
const original = renderPrompt(downstream);
|
|
2569
3092
|
state.currentSystemPrompt = original;
|
|
2570
3093
|
const agent = assembleContext.agent ?? assembleContext.scope;
|
|
2571
3094
|
if (agent !== void 0 && !isSubagentOrigin(agent)) {
|
|
3095
|
+
await runBeforeAgentStart(ctx, state, agent, assembleContext.signal, original);
|
|
2572
3096
|
const override = state.turnSystemPromptOverrides.get(agent);
|
|
2573
3097
|
if (override === void 0) return downstream;
|
|
2574
3098
|
state.currentSystemPrompt = override;
|
|
@@ -2580,6 +3104,7 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2580
3104
|
}]
|
|
2581
3105
|
};
|
|
2582
3106
|
}
|
|
3107
|
+
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2583
3108
|
const event = {
|
|
2584
3109
|
type: "before_agent_start",
|
|
2585
3110
|
prompt: "",
|
|
@@ -2602,9 +3127,69 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2602
3127
|
};
|
|
2603
3128
|
});
|
|
2604
3129
|
}
|
|
2605
|
-
|
|
3130
|
+
/**
|
|
3131
|
+
* Pi's `before_agent_start`, run inside the assembly of the turn it belongs to.
|
|
3132
|
+
*
|
|
3133
|
+
* Fires once per user prompt: only when this step's claim opened a NEW turn,
|
|
3134
|
+
* matching Pi, where the event follows the user's message rather than every
|
|
3135
|
+
* model call. A handler's `systemPrompt` becomes this turn's override (reset
|
|
3136
|
+
* at the next turn, exactly as Pi resets to the base prompt) and a returned
|
|
3137
|
+
* `message` is held for the step that is about to be entered.
|
|
3138
|
+
* @param ctx - context used for attachments and handler dispatch.
|
|
3139
|
+
* @param state - runtime state holding claims, overrides, and injections.
|
|
3140
|
+
* @param agent - the agent whose assembly this is.
|
|
3141
|
+
* @param signal - the turn's control signal, when one exists.
|
|
3142
|
+
* @param assembled - the prompt as assembled, before any package override.
|
|
3143
|
+
*/
|
|
3144
|
+
async function runBeforeAgentStart(ctx, state, agent, signal, assembled) {
|
|
3145
|
+
const claimed = state.claimedForStep.get(agent) ?? [];
|
|
3146
|
+
state.claimedForStep.delete(agent);
|
|
3147
|
+
if (claimed.length === 0) return;
|
|
3148
|
+
const turn = Number(claimed[0]?.turn ?? 0);
|
|
3149
|
+
if (state.promptedTurn.get(agent) === turn) return;
|
|
3150
|
+
state.promptedTurn.set(agent, turn);
|
|
3151
|
+
state.turnSystemPromptOverrides.delete(agent);
|
|
3152
|
+
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return;
|
|
3153
|
+
const userMessages = claimed.map((entry) => entry.message).filter((message) => (message.source?.kind ?? "user") === "user");
|
|
3154
|
+
const prompt = userMessages.flatMap((message) => textBlocks(message.content)).map((block) => block.text).join("\n");
|
|
3155
|
+
const images = await collectPiImages(ctx, userMessages);
|
|
3156
|
+
const event = {
|
|
3157
|
+
type: "before_agent_start",
|
|
3158
|
+
prompt,
|
|
3159
|
+
...images.length > 0 ? { images } : {},
|
|
3160
|
+
systemPrompt: assembled,
|
|
3161
|
+
systemPromptOptions: {}
|
|
3162
|
+
};
|
|
3163
|
+
const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
|
|
3164
|
+
const injected = [];
|
|
3165
|
+
for (const result of results) {
|
|
3166
|
+
if (typeof result !== "object" || result === null) continue;
|
|
3167
|
+
const record = result;
|
|
3168
|
+
if (typeof record.systemPrompt === "string") {
|
|
3169
|
+
state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
|
|
3170
|
+
event.systemPrompt = record.systemPrompt;
|
|
3171
|
+
}
|
|
3172
|
+
const message = record.message;
|
|
3173
|
+
if (message === void 0) continue;
|
|
3174
|
+
const content = message.content;
|
|
3175
|
+
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
3176
|
+
type: "text",
|
|
3177
|
+
text: content
|
|
3178
|
+
}] : content ?? []);
|
|
3179
|
+
injected.push(createUserMessage({
|
|
3180
|
+
content: blocks,
|
|
3181
|
+
source: {
|
|
3182
|
+
kind: "plugin",
|
|
3183
|
+
plugin: state.messageSource,
|
|
3184
|
+
...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
|
|
3185
|
+
}
|
|
3186
|
+
}));
|
|
3187
|
+
}
|
|
3188
|
+
if (injected.length > 0) state.pendingInjections.set(agent, injected);
|
|
3189
|
+
}
|
|
3190
|
+
async function piShapeOfPending(ctx, message) {
|
|
2606
3191
|
const source = message.source;
|
|
2607
|
-
const content = dshToPiContent(message.content ?? []);
|
|
3192
|
+
const content = await dshToPiContent(ctx, message.content ?? []);
|
|
2608
3193
|
return typeof source?.piCustomType === "string" ? {
|
|
2609
3194
|
role: "custom",
|
|
2610
3195
|
customType: source.piCustomType,
|
|
@@ -2619,7 +3204,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2619
3204
|
const session = agentSession(agent);
|
|
2620
3205
|
const history = [];
|
|
2621
3206
|
for (const event of session?.events ?? []) {
|
|
2622
|
-
const projected = messageFromSessionEvent(event);
|
|
3207
|
+
const projected = await messageFromSessionEvent(ctx, event);
|
|
2623
3208
|
if (projected === void 0) continue;
|
|
2624
3209
|
if (event.type === "user/message") {
|
|
2625
3210
|
const customType = (event.data?.source)?.piCustomType;
|
|
@@ -2634,7 +3219,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2634
3219
|
}
|
|
2635
3220
|
history.push(projected);
|
|
2636
3221
|
}
|
|
2637
|
-
const projectedPending = pending.map(piShapeOfPending);
|
|
3222
|
+
const projectedPending = await Promise.all(pending.map((message) => piShapeOfPending(ctx, message)));
|
|
2638
3223
|
const event = {
|
|
2639
3224
|
type: "context",
|
|
2640
3225
|
messages: [...history, ...projectedPending]
|
|
@@ -2681,7 +3266,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2681
3266
|
return rebuilt;
|
|
2682
3267
|
}
|
|
2683
3268
|
async function collectPiImages(ctx, messages) {
|
|
2684
|
-
const attachments = ctx
|
|
3269
|
+
const attachments = optionalService(ctx, "attachments");
|
|
2685
3270
|
if (attachments === void 0) return [];
|
|
2686
3271
|
const images = [];
|
|
2687
3272
|
for (const message of messages) {
|
|
@@ -2905,7 +3490,13 @@ function registerTool(ctx, state, tool) {
|
|
|
2905
3490
|
const mutated = state.argMutations.get(exec);
|
|
2906
3491
|
if (mutated !== void 0) state.argMutations.delete(exec);
|
|
2907
3492
|
const effective = mutated ?? args;
|
|
2908
|
-
const prepared =
|
|
3493
|
+
const prepared = validateToolArguments({
|
|
3494
|
+
name: live.name,
|
|
3495
|
+
parameters: live.parameters
|
|
3496
|
+
}, {
|
|
3497
|
+
name: live.name,
|
|
3498
|
+
arguments: live.prepareArguments?.(cloneJson(effective)) ?? effective
|
|
3499
|
+
});
|
|
2909
3500
|
const agent = exec.agent;
|
|
2910
3501
|
const result = await normalizeToolResultForDsh(ctx, await state.agentScope.run(agent, () => live.execute(String(exec.callId), prepared, exec.signal, (update) => {
|
|
2911
3502
|
dispatch(state, "tool_execution_update", {
|
|
@@ -2943,10 +3534,57 @@ function currentAgent(state) {
|
|
|
2943
3534
|
function toolRuntime(ctx, agent) {
|
|
2944
3535
|
return (agent?.ctx)?.tools ?? ctx.tools;
|
|
2945
3536
|
}
|
|
3537
|
+
/**
|
|
3538
|
+
* The system-prompt text a package's ACTIVE tools contribute.
|
|
3539
|
+
*
|
|
3540
|
+
* Pi renders `promptSnippet` into its "Available tools" list and
|
|
3541
|
+
* `promptGuidelines` into its "Guidelines" bullets. DSH assembles its prompt
|
|
3542
|
+
* from ordered sections, so both arrive as one section in DSH's tool-guidance
|
|
3543
|
+
* band, headed the way Pi heads them.
|
|
3544
|
+
* @param ctx - context used to read which tools are active.
|
|
3545
|
+
* @param state - runtime state holding the registered Pi tools.
|
|
3546
|
+
* @returns the section text, empty when no active Pi tool contributes any.
|
|
3547
|
+
*/
|
|
3548
|
+
function piToolPromptContribution(ctx, state) {
|
|
3549
|
+
const active = new Set(getActiveTools(ctx, state));
|
|
3550
|
+
const snippets = [];
|
|
3551
|
+
const guidelines = [];
|
|
3552
|
+
for (const [name, tool] of state.tools) {
|
|
3553
|
+
if (!active.has(name)) continue;
|
|
3554
|
+
const snippet = tool.promptSnippet;
|
|
3555
|
+
if (typeof snippet === "string" && snippet.trim().length > 0) snippets.push(`- ${snippet.trim()}`);
|
|
3556
|
+
const bullets = tool.promptGuidelines;
|
|
3557
|
+
if (!Array.isArray(bullets)) continue;
|
|
3558
|
+
for (const bullet of bullets) if (typeof bullet === "string" && bullet.trim().length > 0) guidelines.push(`- ${bullet.trim()}`);
|
|
3559
|
+
}
|
|
3560
|
+
const parts = [];
|
|
3561
|
+
if (snippets.length > 0) parts.push(`Available tools:\n${snippets.join("\n")}`);
|
|
3562
|
+
if (guidelines.length > 0) parts.push(`Guidelines:\n${guidelines.join("\n")}`);
|
|
3563
|
+
return parts.join("\n\n");
|
|
3564
|
+
}
|
|
2946
3565
|
function getActiveTools(ctx, state) {
|
|
2947
3566
|
const agent = currentAgent(state);
|
|
2948
3567
|
return toolRuntime(ctx, agent).schemas(agent).map((tool) => tool.name);
|
|
2949
3568
|
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Pi's `setActiveTools`, on DSH's restriction seam.
|
|
3571
|
+
*
|
|
3572
|
+
* Pi walks the requested names and keeps the ones its registry knows —
|
|
3573
|
+
* **unknown names are silently skipped**. DSH's `restrict` instead FAILS the
|
|
3574
|
+
* whole call on a name it cannot restrict (unknown, scope-local, or a
|
|
3575
|
+
* reserved transport name), so passing Pi's list through verbatim turned a
|
|
3576
|
+
* routine Pi call into a hard error over one stale name.
|
|
3577
|
+
*
|
|
3578
|
+
* So the list is narrowed to what DSH says is restrictable before restricting.
|
|
3579
|
+
* The one case that cannot be both: a tool that is VISIBLE but not
|
|
3580
|
+
* restrictable (a scope's own registration, `run_code` outside native mode)
|
|
3581
|
+
* cannot be switched off at all. Silence there would leave a tool running that
|
|
3582
|
+
* the package believes it disabled, so it is reported once — the package's
|
|
3583
|
+
* call still takes effect for everything else.
|
|
3584
|
+
* @param ctx - context used to reach the tool runtime.
|
|
3585
|
+
* @param state - runtime state holding the per-agent restriction disposers.
|
|
3586
|
+
* @param names - the tool names the package wants active.
|
|
3587
|
+
*/
|
|
2950
3588
|
function setActiveTools(ctx, state, names) {
|
|
2951
3589
|
const unique = [...new Set(names)];
|
|
2952
3590
|
const agent = currentAgent(state);
|
|
@@ -2957,27 +3595,104 @@ function setActiveTools(ctx, state, names) {
|
|
|
2957
3595
|
logger(ctx).warn("[pi2dsh] setActiveTools deferred: the current agent exposes no scoped tools.restrict()");
|
|
2958
3596
|
return;
|
|
2959
3597
|
}
|
|
3598
|
+
const restrictable = restrictableToolNames(scopedTools, agent);
|
|
3599
|
+
const allow = restrictable === void 0 ? unique : unique.filter((name) => restrictable.has(name));
|
|
3600
|
+
if (restrictable !== void 0) {
|
|
3601
|
+
const unswitchable = [...new Set(scopedTools.schemas(agent).map((schema) => schema.name))].filter((name) => !restrictable.has(name) && !unique.includes(name));
|
|
3602
|
+
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`);
|
|
3603
|
+
}
|
|
2960
3604
|
state.toolRestrictions.get(agent)?.();
|
|
2961
|
-
state.toolRestrictions.set(agent, scopedTools.restrict({
|
|
3605
|
+
state.toolRestrictions.set(agent, allow.length === 0 ? scopedTools.restrict({ deny: [...restrictable ?? /* @__PURE__ */ new Set()] }) : scopedTools.restrict({ allow }));
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* The tool names DSH will accept in a restriction for this scope.
|
|
3609
|
+
* @param tools - the agent-scoped tool runtime.
|
|
3610
|
+
* @param agent - the scope key.
|
|
3611
|
+
* @returns the restrictable names, or undefined when this runtime exposes no view.
|
|
3612
|
+
*/
|
|
3613
|
+
function restrictableToolNames(tools, agent) {
|
|
3614
|
+
const view = tools.view;
|
|
3615
|
+
if (typeof view !== "function") return void 0;
|
|
3616
|
+
try {
|
|
3617
|
+
const names = view.call(tools, agent)?.restrictableNames;
|
|
3618
|
+
return names instanceof Set ? names : void 0;
|
|
3619
|
+
} catch {
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
2962
3622
|
}
|
|
2963
3623
|
function deliverAgentMessage(agent, message, mode) {
|
|
2964
3624
|
const deliver = agent[mode];
|
|
2965
3625
|
if (typeof deliver !== "function") throw new Error(`pi2dsh: active DSH agent has no ${mode}() delivery method`);
|
|
2966
3626
|
deliver.call(agent, message);
|
|
2967
3627
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
3628
|
+
/**
|
|
3629
|
+
* Pi's `sendMessage` / `sendUserMessage`, on DSH.
|
|
3630
|
+
*
|
|
3631
|
+
* Two things this has to get right that a plain `deliverAgentMessage` does not:
|
|
3632
|
+
*
|
|
3633
|
+
* - **Durability.** Pi's no-turn `sendMessage` appends to the session and
|
|
3634
|
+
* emits its message events before it returns: on return the message IS in
|
|
3635
|
+
* the conversation. DSH's inject only queues it in the agent's inbox, where
|
|
3636
|
+
* it becomes a `user/message` when the next step claims it — so a turn
|
|
3637
|
+
* cancelled in between dropped it, and a package that had already reported
|
|
3638
|
+
* success was wrong. The no-turn mode now appends to the durable log
|
|
3639
|
+
* itself, which is what Pi's contract promises.
|
|
3640
|
+
* - **Which session.** Inside a `withSession` callback the context is bound to
|
|
3641
|
+
* the REPLACEMENT session, but the live agent is still the one that started
|
|
3642
|
+
* the operation — so routing through the agent wrote into the OLD session.
|
|
3643
|
+
* An override session is written to directly.
|
|
3644
|
+
* @param ctx - context used for content conversion.
|
|
3645
|
+
* @param state - runtime state (message source, active agent).
|
|
3646
|
+
* @param content - the Pi content to deliver.
|
|
3647
|
+
* @param mode - inject (no turn), steer, or followup.
|
|
3648
|
+
* @param sessionOverride - the replacement session, inside a withSession callback.
|
|
3649
|
+
* @param customType - Pi's role:"custom" marker, when the caller sent one.
|
|
3650
|
+
*/
|
|
3651
|
+
async function sendPiMessage(ctx, state, content, mode, sessionOverride, customType) {
|
|
2970
3652
|
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2971
3653
|
type: "text",
|
|
2972
3654
|
text: content
|
|
2973
3655
|
}] : content);
|
|
2974
|
-
|
|
3656
|
+
const message = createUserMessage({
|
|
2975
3657
|
content: blocks,
|
|
2976
3658
|
source: {
|
|
2977
3659
|
kind: "plugin",
|
|
2978
|
-
plugin: state.messageSource
|
|
3660
|
+
plugin: state.messageSource,
|
|
3661
|
+
...customType === void 0 ? {} : { piCustomType: customType }
|
|
2979
3662
|
}
|
|
2980
|
-
})
|
|
3663
|
+
});
|
|
3664
|
+
if (sessionOverride !== void 0) {
|
|
3665
|
+
appendUserMessage(sessionOverride, message);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (mode === "inject") {
|
|
3669
|
+
const session = agentSession(currentAgent(state));
|
|
3670
|
+
if (session === void 0) throw new Error("pi2dsh: sendMessage requires one active DSH agent with a durable session");
|
|
3671
|
+
appendUserMessage(session, message);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
deliverAgentMessage(requireAgent(state, "sendUserMessage"), message, mode);
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Pi's delivery options → this bridge's mode. No options at all means "into
|
|
3678
|
+
* the conversation, no turn", which is Pi's own default.
|
|
3679
|
+
* @param options - the caller's delivery options.
|
|
3680
|
+
*/
|
|
3681
|
+
function deliveryMode(options) {
|
|
3682
|
+
if (options.deliverAs === "steer") return "steer";
|
|
3683
|
+
if (options.deliverAs === "followUp" || options.deliverAs === "nextTurn" || options.triggerTurn === true) return "followup";
|
|
3684
|
+
return "inject";
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Append one plugin-sourced user message to a session's durable log, which is
|
|
3688
|
+
* what makes it part of the conversation before the call returns.
|
|
3689
|
+
* @param session - the live DSH session to append to.
|
|
3690
|
+
* @param message - the message, already in DSH shape.
|
|
3691
|
+
*/
|
|
3692
|
+
function appendUserMessage(session, message) {
|
|
3693
|
+
const append = session.append;
|
|
3694
|
+
if (typeof append !== "function") throw new Error("pi2dsh: this session cannot be appended to, so the message could not be delivered durably");
|
|
3695
|
+
append.call(session, "user/message", message, { surfaceOp: "append" });
|
|
2981
3696
|
}
|
|
2982
3697
|
function combineExecSignal(options) {
|
|
2983
3698
|
const controller = new AbortController();
|
|
@@ -3087,6 +3802,51 @@ function registerCommand(ctx, state, command) {
|
|
|
3087
3802
|
}
|
|
3088
3803
|
logger(ctx).warn(`[pi2dsh] command /${command.name} was not registered: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
3089
3804
|
}
|
|
3805
|
+
/**
|
|
3806
|
+
* The session's current name: DSH's title when a title service is mounted,
|
|
3807
|
+
* this bridge's sidecar otherwise (and as the fallback for a session named
|
|
3808
|
+
* before the service was there).
|
|
3809
|
+
* @param ctx - the cordis context to resolve the optional service from.
|
|
3810
|
+
* @param state - runtime state holding the sidecar.
|
|
3811
|
+
* @param session - the live DSH session.
|
|
3812
|
+
*/
|
|
3813
|
+
function sessionNameOf(ctx, state, session) {
|
|
3814
|
+
const title = optionalService(ctx, "sessionTitle")?.get(session)?.title;
|
|
3815
|
+
return typeof title === "string" && title.length > 0 ? title : state.bridge.getName(session.id);
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Whether a human can actually answer a question right now.
|
|
3819
|
+
*
|
|
3820
|
+
* Two facts decide it, and the old check (`the service is mounted`) saw
|
|
3821
|
+
* neither: a headless composition mounts the service and registers NO
|
|
3822
|
+
* provider, and a delegated child agent has no human answerer at all — both
|
|
3823
|
+
* make every `ctx.ui.select/confirm/input` throw for a package that was told
|
|
3824
|
+
* `hasUI: true` and skipped its non-interactive path.
|
|
3825
|
+
*
|
|
3826
|
+
* The provider is a private field, but its existence is publicly observable:
|
|
3827
|
+
* DSH allows exactly one provider per context and refuses a second with
|
|
3828
|
+
* `DUPLICATE_PROVIDER`. So registering a probe answers the question — a
|
|
3829
|
+
* refusal means a real provider is there, and an acceptance means there was
|
|
3830
|
+
* none, which the immediate disposal restores.
|
|
3831
|
+
* @param userQuestions - the mounted question service, if any.
|
|
3832
|
+
* @param agent - the agent whose turn this context belongs to.
|
|
3833
|
+
*/
|
|
3834
|
+
function humanAnswererAvailable(userQuestions, agent) {
|
|
3835
|
+
if (userQuestions === void 0) return false;
|
|
3836
|
+
if (isSubagentOrigin(agent)) return false;
|
|
3837
|
+
const service = userQuestions;
|
|
3838
|
+
if (typeof service.registerProvider !== "function") return false;
|
|
3839
|
+
let dispose;
|
|
3840
|
+
try {
|
|
3841
|
+
dispose = service.registerProvider({ ask: async () => {
|
|
3842
|
+
throw new Error("pi2dsh probe provider");
|
|
3843
|
+
} });
|
|
3844
|
+
} catch {
|
|
3845
|
+
return true;
|
|
3846
|
+
}
|
|
3847
|
+
dispose?.();
|
|
3848
|
+
return false;
|
|
3849
|
+
}
|
|
3090
3850
|
function requireSession(state, operation) {
|
|
3091
3851
|
const session = agentSession(currentAgent(state));
|
|
3092
3852
|
if (session === void 0) throw new Error(`pi2dsh: ${operation} requires one active DSH agent with a durable session`);
|
|
@@ -3135,7 +3895,8 @@ function createPiApi(ctx, state) {
|
|
|
3135
3895
|
providerConfig: value,
|
|
3136
3896
|
store: oauthStoreOf(state)
|
|
3137
3897
|
}),
|
|
3138
|
-
warn: (message) => logger(ctx).warn(message)
|
|
3898
|
+
warn: (message) => logger(ctx).warn(message),
|
|
3899
|
+
resolveAttachments: () => optionalService(ctx, "attachments")
|
|
3139
3900
|
}
|
|
3140
3901
|
});
|
|
3141
3902
|
if (routeDisposer !== void 0) {
|
|
@@ -3190,8 +3951,7 @@ function createPiApi(ctx, state) {
|
|
|
3190
3951
|
},
|
|
3191
3952
|
sendMessage(message, options = {}) {
|
|
3192
3953
|
requireAgent(state, "sendMessage");
|
|
3193
|
-
|
|
3194
|
-
return sendPiMessage(ctx, state, message.content, mode);
|
|
3954
|
+
return sendPiMessage(ctx, state, message.content, deliveryMode(options), void 0, typeof message.customType === "string" ? message.customType : void 0);
|
|
3195
3955
|
},
|
|
3196
3956
|
sendUserMessage(content, options = {}) {
|
|
3197
3957
|
requireAgent(state, "sendUserMessage");
|
|
@@ -3203,15 +3963,17 @@ function createPiApi(ctx, state) {
|
|
|
3203
3963
|
},
|
|
3204
3964
|
setSessionName(name) {
|
|
3205
3965
|
const session = requireSession(state, "setSessionName");
|
|
3206
|
-
|
|
3966
|
+
const titles = optionalService(ctx, "sessionTitle");
|
|
3967
|
+
if (titles !== void 0 && String(name).trim().length > 0) titles.rename(session, String(name));
|
|
3968
|
+
else state.bridge.setName(session.id, String(name));
|
|
3207
3969
|
dispatch(state, "session_info_changed", {
|
|
3208
3970
|
type: "session_info_changed",
|
|
3209
|
-
name: state
|
|
3971
|
+
name: sessionNameOf(ctx, state, session)
|
|
3210
3972
|
}, contextFor(ctx, state, currentAgent(state), void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] session_info_changed handler failed: ${String(error)}`));
|
|
3211
3973
|
},
|
|
3212
3974
|
getSessionName() {
|
|
3213
3975
|
const session = agentSession(currentAgent(state));
|
|
3214
|
-
return session === void 0 ? void 0 : state
|
|
3976
|
+
return session === void 0 ? void 0 : sessionNameOf(ctx, state, session);
|
|
3215
3977
|
},
|
|
3216
3978
|
setLabel(entryId, label) {
|
|
3217
3979
|
const session = requireSession(state, "setLabel");
|
|
@@ -3258,11 +4020,12 @@ function createPiApi(ctx, state) {
|
|
|
3258
4020
|
...typeof model?.id === "string" ? { model: model.id } : {}
|
|
3259
4021
|
};
|
|
3260
4022
|
if (override.model === void 0) return false;
|
|
4023
|
+
const previousModel = currentPiModel(state, agent);
|
|
3261
4024
|
state.modelOverrides.set(agent, override);
|
|
3262
4025
|
dispatch(state, "model_select", {
|
|
3263
4026
|
type: "model_select",
|
|
3264
4027
|
model,
|
|
3265
|
-
previousModel
|
|
4028
|
+
previousModel,
|
|
3266
4029
|
source: "set"
|
|
3267
4030
|
}, contextFor(ctx, state, agent, void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] model_select handler failed: ${String(error)}`));
|
|
3268
4031
|
return true;
|
|
@@ -3460,6 +4223,12 @@ async function applyPiPackage(ctx, options) {
|
|
|
3460
4223
|
modelOverrides: /* @__PURE__ */ new WeakMap(),
|
|
3461
4224
|
thinkingLevels: /* @__PURE__ */ new WeakMap(),
|
|
3462
4225
|
turnSystemPromptOverrides: /* @__PURE__ */ new WeakMap(),
|
|
4226
|
+
projection: Promise.resolve(),
|
|
4227
|
+
terminateBatch: /* @__PURE__ */ new WeakMap(),
|
|
4228
|
+
piTurnIndex: /* @__PURE__ */ new WeakMap(),
|
|
4229
|
+
claimedForStep: /* @__PURE__ */ new WeakMap(),
|
|
4230
|
+
promptedTurn: /* @__PURE__ */ new WeakMap(),
|
|
4231
|
+
pendingInjections: /* @__PURE__ */ new WeakMap(),
|
|
3463
4232
|
globalThinkingLevel: "off",
|
|
3464
4233
|
argMutations: /* @__PURE__ */ new WeakMap(),
|
|
3465
4234
|
streamingTexts: /* @__PURE__ */ new Map(),
|
|
@@ -3520,7 +4289,7 @@ async function applyPiPackage(ctx, options) {
|
|
|
3520
4289
|
},
|
|
3521
4290
|
piContentToDsh: (content) => piToDshContent(ctx, content),
|
|
3522
4291
|
deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
|
|
3523
|
-
messageFromSessionEvent,
|
|
4292
|
+
messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
|
|
3524
4293
|
messageSource: state.messageSource,
|
|
3525
4294
|
packageName: state.packageName
|
|
3526
4295
|
}, subagentOptions));
|
|
@@ -3556,7 +4325,10 @@ async function applyPiPackage(ctx, options) {
|
|
|
3556
4325
|
logger(ctx).info(`[pi2dsh] loaded ${options.manifest.package.name}: ${state.tools.size} tools, ${state.commands.size} commands, ${options.manifest.skillDirs.length} skill roots${healthSuffix}`);
|
|
3557
4326
|
}
|
|
3558
4327
|
const runtimeInternals = {
|
|
4328
|
+
compactionReason,
|
|
4329
|
+
dshToPiContent,
|
|
3559
4330
|
expandPrompt,
|
|
4331
|
+
isSubagentOrigin,
|
|
3560
4332
|
normalizeToolResult,
|
|
3561
4333
|
splitArguments,
|
|
3562
4334
|
textBlocks
|
|
@@ -3564,4 +4336,4 @@ const runtimeInternals = {
|
|
|
3564
4336
|
//#endregion
|
|
3565
4337
|
export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
|
|
3566
4338
|
|
|
3567
|
-
//# sourceMappingURL=runtime-
|
|
4339
|
+
//# sourceMappingURL=runtime-DsPaznXJ.mjs.map
|