pi2dsh 0.12.1 → 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-Xy8yU8wO.mjs → runtime-DsPaznXJ.mjs} +977 -219
- 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-Xy8yU8wO.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import { F as AssistantMessageEventStream, a as builtinProviders, h as getSupportedThinkingLevels, i as __setPiAiLlmBridge } from "./pi-ai-Dyg4zyLZ.mjs";
|
|
3
|
-
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-
|
|
2
|
+
import { F as AssistantMessageEventStream, I as isContextOverflow, a as builtinProviders, h as getSupportedThinkingLevels, i as __setPiAiLlmBridge } from "./pi-ai-Dyg4zyLZ.mjs";
|
|
3
|
+
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-CYnmeJEx.mjs";
|
|
4
4
|
import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
|
|
5
5
|
import { a as resolvePiProviderAuth, n as loginPiProvider, r as providerSupportsOAuth, t as FileCredentialStore } from "./oauth-bridge-CfyImWx4.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -12,10 +12,18 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
14
|
import { createJiti } from "jiti";
|
|
15
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
15
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
16
16
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { foldSurface } from "@deepseek-ai/dsh-session";
|
|
19
|
+
import { Compile } from "typebox/compile";
|
|
20
|
+
import { Value } from "typebox/value";
|
|
18
21
|
//#region src/session-bridge.ts
|
|
22
|
+
/** The durable seq behind a projected entry id (`dsh-<seq>`), when it has one. */
|
|
23
|
+
function entrySeq(id) {
|
|
24
|
+
const match = /^dsh-(\d+)$/.exec(id);
|
|
25
|
+
return match === null ? void 0 : Number(match[1]);
|
|
26
|
+
}
|
|
19
27
|
function sidecarDir() {
|
|
20
28
|
return join(getAgentDir(), "session-entries");
|
|
21
29
|
}
|
|
@@ -143,6 +151,20 @@ var PiSessionBridge = class {
|
|
|
143
151
|
* shape. DSH history is linear, so the projection is a single-branch tree:
|
|
144
152
|
* every entry's parent is its predecessor.
|
|
145
153
|
*/
|
|
154
|
+
/**
|
|
155
|
+
* The seqs still on the model-visible surface, via DSH's own canonical fold.
|
|
156
|
+
* @param session - the session to project.
|
|
157
|
+
* @returns the visible seqs, or undefined when the fold cannot run (a
|
|
158
|
+
* projection built from a partial event list, e.g. in a test double) — in
|
|
159
|
+
* which case nothing is filtered out rather than everything.
|
|
160
|
+
*/
|
|
161
|
+
visibleSeqs(session) {
|
|
162
|
+
try {
|
|
163
|
+
return new Set(foldSurface(sessionEvents(session)).nodes);
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
146
168
|
projectEntries(session) {
|
|
147
169
|
this.load(session.id);
|
|
148
170
|
const merged = [];
|
|
@@ -270,7 +292,14 @@ var PiSessionBridge = class {
|
|
|
270
292
|
const index = entries.findIndex((entry) => entry.id === fromId);
|
|
271
293
|
return index === -1 ? [] : entries.slice(0, index + 1);
|
|
272
294
|
},
|
|
273
|
-
buildContextEntries: () =>
|
|
295
|
+
buildContextEntries: () => {
|
|
296
|
+
const visible = this.visibleSeqs(session);
|
|
297
|
+
return entriesOf().filter((entry) => {
|
|
298
|
+
if (entry.type !== "message" && entry.type !== "compaction" && entry.type !== "branch_summary" && entry.type !== "custom_message") return false;
|
|
299
|
+
const seq = entrySeq(entry.id);
|
|
300
|
+
return seq === void 0 || visible === void 0 || visible.has(seq);
|
|
301
|
+
});
|
|
302
|
+
},
|
|
274
303
|
getHeader: () => ({
|
|
275
304
|
type: "session",
|
|
276
305
|
version: 3,
|
|
@@ -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 {
|
|
@@ -1329,6 +1721,12 @@ function piCarriedFields(model) {
|
|
|
1329
1721
|
* Structural typing carries it through `llm.registerAdapter`.
|
|
1330
1722
|
*/
|
|
1331
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
|
+
};
|
|
1332
1730
|
return {
|
|
1333
1731
|
providerInfo: (id) => ({
|
|
1334
1732
|
id,
|
|
@@ -1362,7 +1760,14 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1362
1760
|
id: modelId,
|
|
1363
1761
|
provider: providerId
|
|
1364
1762
|
};
|
|
1365
|
-
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;
|
|
1366
1771
|
const piOptions = {
|
|
1367
1772
|
...auth?.apiKey === void 0 ? {} : { apiKey: auth.apiKey },
|
|
1368
1773
|
...auth?.baseUrl === void 0 ? {} : { baseUrl: auth.baseUrl },
|
|
@@ -1370,9 +1775,20 @@ function piProviderDshAdapter(providerId, provider, host) {
|
|
|
1370
1775
|
...options.signal instanceof AbortSignal ? { signal: options.signal } : {},
|
|
1371
1776
|
...typeof options.maxTokens === "number" ? { maxTokens: options.maxTokens } : {},
|
|
1372
1777
|
...typeof options.temperature === "number" ? { temperature: options.temperature } : {},
|
|
1373
|
-
...typeof options.
|
|
1778
|
+
...typeof options.sessionId === "string" ? { sessionId: options.sessionId } : {},
|
|
1779
|
+
...effort === void 0 || effort === "off" ? {} : { reasoning: effort }
|
|
1374
1780
|
};
|
|
1375
|
-
|
|
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);
|
|
1376
1792
|
}
|
|
1377
1793
|
};
|
|
1378
1794
|
}
|
|
@@ -1786,11 +2202,12 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
1786
2202
|
const notices = [];
|
|
1787
2203
|
const userQuestions = optionalService(ctx, "userQuestions");
|
|
1788
2204
|
const ui = {
|
|
1789
|
-
notify(message) {
|
|
2205
|
+
notify(message, type) {
|
|
1790
2206
|
const text = String(message);
|
|
1791
2207
|
notices.push(text);
|
|
1792
2208
|
state.notifications.push(text);
|
|
1793
|
-
|
|
2209
|
+
const level = type === "warning" || type === "error" ? "warn" : "info";
|
|
2210
|
+
logger(ctx)[level](`[pi2dsh] ${text}`);
|
|
1794
2211
|
},
|
|
1795
2212
|
select: (title, options) => askOne(ctx, agent, signal, {
|
|
1796
2213
|
id: "pi2dsh-select",
|
|
@@ -2003,9 +2420,18 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2003
2420
|
};
|
|
2004
2421
|
const contextCwd = typeof (sessionOverride?.header)?.cwd === "string" ? sessionOverride.header.cwd : cwdOf(agent);
|
|
2005
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
|
+
},
|
|
2006
2430
|
ui,
|
|
2007
2431
|
mode: "rpc",
|
|
2008
|
-
hasUI
|
|
2432
|
+
get hasUI() {
|
|
2433
|
+
return humanAnswererAvailable(userQuestions, agent);
|
|
2434
|
+
},
|
|
2009
2435
|
cwd: contextCwd,
|
|
2010
2436
|
sessionManager: session === void 0 ? state.bridge.readonlySessionManager({
|
|
2011
2437
|
id: "pi2dsh-detached",
|
|
@@ -2013,7 +2439,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2013
2439
|
}, contextCwd) : state.bridge.readonlySessionManager(session, contextCwd),
|
|
2014
2440
|
modelRegistry,
|
|
2015
2441
|
model: agent === void 0 ? void 0 : currentPiModel(state, agent),
|
|
2016
|
-
scopedModels:
|
|
2442
|
+
scopedModels: [],
|
|
2017
2443
|
thinkingLevel: thinkingLevelOf(state, agent),
|
|
2018
2444
|
isIdle: () => command,
|
|
2019
2445
|
isProjectTrusted: () => false,
|
|
@@ -2026,7 +2452,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2026
2452
|
reason: "pi2dsh: aborted by migrated Pi extension"
|
|
2027
2453
|
});
|
|
2028
2454
|
},
|
|
2029
|
-
hasPendingMessages: () =>
|
|
2455
|
+
hasPendingMessages: () => agent?.inbox?.hasPending === true,
|
|
2030
2456
|
shutdown: () => {
|
|
2031
2457
|
capabilityLedgerOf(ctx, state).reportHostDecision({
|
|
2032
2458
|
capability: "ctx.shutdown",
|
|
@@ -2228,39 +2654,93 @@ async function dispatch(state, eventName, event, eventContext) {
|
|
|
2228
2654
|
for (const handler of state.handlers.get(eventName) ?? []) results.push(await state.agentScope.run(agent, () => handler(event, eventContext)));
|
|
2229
2655
|
return results;
|
|
2230
2656
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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
|
|
2248
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;
|
|
2249
2708
|
}
|
|
2250
|
-
|
|
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
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
async function messageFromSessionEvent(ctx, event) {
|
|
2251
2731
|
const type = event.type;
|
|
2252
2732
|
const data = event.data;
|
|
2253
2733
|
if (typeof data !== "object" || data === null) return void 0;
|
|
2254
2734
|
const record = data;
|
|
2255
2735
|
if (type === "user/message") return {
|
|
2256
2736
|
role: "user",
|
|
2257
|
-
content: dshToPiContent(record.content ?? [])
|
|
2737
|
+
content: await dshToPiContent(ctx, record.content ?? [])
|
|
2258
2738
|
};
|
|
2259
2739
|
if (type === "assistant/message") {
|
|
2260
2740
|
const message = record.message;
|
|
2261
2741
|
return {
|
|
2262
2742
|
role: "assistant",
|
|
2263
|
-
content: dshToPiContent(message?.content ?? [])
|
|
2743
|
+
content: await dshToPiContent(ctx, message?.content ?? [])
|
|
2264
2744
|
};
|
|
2265
2745
|
}
|
|
2266
2746
|
if (type === "tool/result") {
|
|
@@ -2268,7 +2748,7 @@ function messageFromSessionEvent(event) {
|
|
|
2268
2748
|
return {
|
|
2269
2749
|
role: "toolResult",
|
|
2270
2750
|
toolCallId: tool?.toolCallId,
|
|
2271
|
-
content: dshToPiContent(tool?.content ?? []),
|
|
2751
|
+
content: await dshToPiContent(ctx, tool?.content ?? []),
|
|
2272
2752
|
isError: tool?.isError === true
|
|
2273
2753
|
};
|
|
2274
2754
|
}
|
|
@@ -2315,13 +2795,42 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2315
2795
|
const eventContext = contextFor(ctx, state, agent, void 0);
|
|
2316
2796
|
const type = event.type;
|
|
2317
2797
|
if (type === "turn/start") {
|
|
2318
|
-
|
|
2798
|
+
if (session !== void 0) state.piTurnIndex.set(session, 0);
|
|
2319
2799
|
dispatch(state, "agent_start", { type: "agent_start" }, eventContext).catch((error) => warn("agent_start", error));
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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));
|
|
2325
2834
|
}
|
|
2326
2835
|
if (type === "tool/call") {
|
|
2327
2836
|
const data = event.data;
|
|
@@ -2372,21 +2881,26 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2372
2881
|
type: "session_before_compact",
|
|
2373
2882
|
preparation: { ...event.data },
|
|
2374
2883
|
branchEntries: [],
|
|
2375
|
-
reason:
|
|
2884
|
+
reason: compactionReason(event.data),
|
|
2376
2885
|
willRetry: false
|
|
2377
2886
|
}, eventContext).catch((error) => warn("session_before_compact", error));
|
|
2378
|
-
if (type === "compaction/
|
|
2887
|
+
if (type === "compaction/summary") {
|
|
2379
2888
|
const data = event.data;
|
|
2889
|
+
const shadowed = data.shadowedRange;
|
|
2890
|
+
const usage = data.usage;
|
|
2380
2891
|
dispatch(state, "session_compact", {
|
|
2381
2892
|
type: "session_compact",
|
|
2382
2893
|
compactionEntry: {
|
|
2383
2894
|
type: "compaction",
|
|
2384
2895
|
id: `dsh-${String(event.seq ?? "")}`,
|
|
2385
|
-
summary:
|
|
2386
|
-
|
|
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
|
|
2387
2901
|
},
|
|
2388
2902
|
fromExtension: false,
|
|
2389
|
-
reason:
|
|
2903
|
+
reason: compactionReason(data),
|
|
2390
2904
|
willRetry: false
|
|
2391
2905
|
}, eventContext).catch((error) => warn("session_compact", error));
|
|
2392
2906
|
}
|
|
@@ -2404,29 +2918,25 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2404
2918
|
state.lastLoggedModels.set(agent, String(model));
|
|
2405
2919
|
}
|
|
2406
2920
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
role: "assistant",
|
|
2422
|
-
content: []
|
|
2423
|
-
},
|
|
2424
|
-
toolResults: []
|
|
2425
|
-
}, eventContext).then(() => dispatch(state, "agent_end", {
|
|
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", {
|
|
2426
2935
|
type: "agent_end",
|
|
2427
2936
|
messages: []
|
|
2428
|
-
}, eventContext)
|
|
2429
|
-
|
|
2937
|
+
}, eventContext);
|
|
2938
|
+
await dispatch(state, "agent_settled", { type: "agent_settled" }, eventContext);
|
|
2939
|
+
}).catch((error) => warn("turn end lifecycle", error));
|
|
2430
2940
|
});
|
|
2431
2941
|
cordis.on("agent/request", async (payload, next) => {
|
|
2432
2942
|
const config = await next();
|
|
@@ -2442,19 +2952,6 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2442
2952
|
...thinking === void 0 || thinking === "off" ? {} : { reasoningEffort: thinking }
|
|
2443
2953
|
};
|
|
2444
2954
|
});
|
|
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
2955
|
cordis.effect(() => async () => {
|
|
2459
2956
|
for (const agent of state.activeAgents) if (typeof agent === "object" && agent !== null && !state.disposedAgents.has(agent)) {
|
|
2460
2957
|
state.disposedAgents.add(agent);
|
|
@@ -2472,6 +2969,7 @@ function subscribeLifecycle(ctx, state) {
|
|
|
2472
2969
|
function subscribeInterceptors(ctx, state) {
|
|
2473
2970
|
const cordis = ctx;
|
|
2474
2971
|
cordis.on("tools/pre-execute", async (exec, next) => {
|
|
2972
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2475
2973
|
const input = cloneJson(exec.arguments);
|
|
2476
2974
|
const event = {
|
|
2477
2975
|
type: "tool_call",
|
|
@@ -2487,21 +2985,47 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2487
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`
|
|
2488
2986
|
};
|
|
2489
2987
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2988
|
+
const agent = exec.agent;
|
|
2989
|
+
const tally = agent === void 0 ? void 0 : state.terminateBatch.get(agent) ?? {
|
|
2990
|
+
calls: 0,
|
|
2991
|
+
terminating: 0
|
|
2493
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
|
+
}
|
|
2494
3007
|
return next();
|
|
2495
3008
|
});
|
|
2496
3009
|
cordis.on("tools/post-execute", async (exec, result, next) => {
|
|
3010
|
+
if (isSubagentOrigin(exec.agent)) return next();
|
|
2497
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)}`));
|
|
2498
3022
|
if (downstream.kind === "block") return downstream;
|
|
2499
3023
|
const event = {
|
|
2500
3024
|
type: "tool_result",
|
|
2501
3025
|
toolName: exec.name,
|
|
2502
3026
|
toolCallId: exec.callId,
|
|
2503
3027
|
input: cloneJson(exec.arguments),
|
|
2504
|
-
content: dshToPiContent(result.content),
|
|
3028
|
+
content: await dshToPiContent(ctx, result.content),
|
|
2505
3029
|
details: result.meta ?? null,
|
|
2506
3030
|
isError: result.isError,
|
|
2507
3031
|
usage: void 0
|
|
@@ -2517,57 +3041,37 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2517
3041
|
feedback: content
|
|
2518
3042
|
};
|
|
2519
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");
|
|
2520
|
-
if (!jsonEqual(event.content, dshToPiContent(result.content))) return {
|
|
3044
|
+
if (!jsonEqual(event.content, await dshToPiContent(ctx, result.content))) return {
|
|
2521
3045
|
kind: "accept",
|
|
2522
3046
|
content
|
|
2523
3047
|
};
|
|
2524
3048
|
return downstream;
|
|
2525
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
|
+
});
|
|
2526
3060
|
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
3061
|
const agent = payload.agent;
|
|
2531
|
-
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;
|
|
2532
3071
|
const signal = payload.signal;
|
|
2533
3072
|
const messages = decision.messages ?? [];
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
const images = await collectPiImages(ctx, userMessages);
|
|
2537
|
-
state.turnSystemPromptOverrides.delete(agent);
|
|
2538
|
-
const event = {
|
|
2539
|
-
type: "before_agent_start",
|
|
2540
|
-
prompt,
|
|
2541
|
-
...images.length > 0 ? { images } : {},
|
|
2542
|
-
systemPrompt: state.currentSystemPrompt,
|
|
2543
|
-
systemPromptOptions: {}
|
|
2544
|
-
};
|
|
2545
|
-
const results = await dispatch(state, "before_agent_start", event, contextFor(ctx, state, agent, signal));
|
|
2546
|
-
const injected = [];
|
|
2547
|
-
for (const result of results) {
|
|
2548
|
-
if (typeof result !== "object" || result === null) continue;
|
|
2549
|
-
const record = result;
|
|
2550
|
-
if (typeof record.systemPrompt === "string") {
|
|
2551
|
-
state.turnSystemPromptOverrides.set(agent, record.systemPrompt);
|
|
2552
|
-
event.systemPrompt = record.systemPrompt;
|
|
2553
|
-
}
|
|
2554
|
-
const message = record.message;
|
|
2555
|
-
if (message !== void 0) {
|
|
2556
|
-
const content = message.content;
|
|
2557
|
-
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2558
|
-
type: "text",
|
|
2559
|
-
text: content
|
|
2560
|
-
}] : content ?? []);
|
|
2561
|
-
injected.push(createUserMessage({
|
|
2562
|
-
content: blocks,
|
|
2563
|
-
source: {
|
|
2564
|
-
kind: "plugin",
|
|
2565
|
-
plugin: state.messageSource,
|
|
2566
|
-
...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
|
|
2567
|
-
}
|
|
2568
|
-
}));
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
3073
|
+
const injected = state.pendingInjections.get(agent) ?? [];
|
|
3074
|
+
state.pendingInjections.delete(agent);
|
|
2571
3075
|
const stepMessages = injected.length === 0 ? messages : [...messages, ...injected];
|
|
2572
3076
|
const transformed = await applyPiContextTransform(ctx, state, agent, signal, stepMessages);
|
|
2573
3077
|
if (transformed === stepMessages && injected.length === 0) return decision;
|
|
@@ -2576,13 +3080,19 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2576
3080
|
messages: transformed
|
|
2577
3081
|
};
|
|
2578
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
|
+
}));
|
|
2579
3089
|
cordis.on("system-prompt/assemble", async (assembly, assembleContext, next) => {
|
|
2580
3090
|
const downstream = await next();
|
|
2581
|
-
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2582
3091
|
const original = renderPrompt(downstream);
|
|
2583
3092
|
state.currentSystemPrompt = original;
|
|
2584
3093
|
const agent = assembleContext.agent ?? assembleContext.scope;
|
|
2585
3094
|
if (agent !== void 0 && !isSubagentOrigin(agent)) {
|
|
3095
|
+
await runBeforeAgentStart(ctx, state, agent, assembleContext.signal, original);
|
|
2586
3096
|
const override = state.turnSystemPromptOverrides.get(agent);
|
|
2587
3097
|
if (override === void 0) return downstream;
|
|
2588
3098
|
state.currentSystemPrompt = override;
|
|
@@ -2594,6 +3104,7 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2594
3104
|
}]
|
|
2595
3105
|
};
|
|
2596
3106
|
}
|
|
3107
|
+
if ((state.handlers.get("before_agent_start")?.length ?? 0) === 0) return downstream;
|
|
2597
3108
|
const event = {
|
|
2598
3109
|
type: "before_agent_start",
|
|
2599
3110
|
prompt: "",
|
|
@@ -2616,9 +3127,69 @@ function subscribeInterceptors(ctx, state) {
|
|
|
2616
3127
|
};
|
|
2617
3128
|
});
|
|
2618
3129
|
}
|
|
2619
|
-
|
|
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) {
|
|
2620
3191
|
const source = message.source;
|
|
2621
|
-
const content = dshToPiContent(message.content ?? []);
|
|
3192
|
+
const content = await dshToPiContent(ctx, message.content ?? []);
|
|
2622
3193
|
return typeof source?.piCustomType === "string" ? {
|
|
2623
3194
|
role: "custom",
|
|
2624
3195
|
customType: source.piCustomType,
|
|
@@ -2633,7 +3204,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2633
3204
|
const session = agentSession(agent);
|
|
2634
3205
|
const history = [];
|
|
2635
3206
|
for (const event of session?.events ?? []) {
|
|
2636
|
-
const projected = messageFromSessionEvent(event);
|
|
3207
|
+
const projected = await messageFromSessionEvent(ctx, event);
|
|
2637
3208
|
if (projected === void 0) continue;
|
|
2638
3209
|
if (event.type === "user/message") {
|
|
2639
3210
|
const customType = (event.data?.source)?.piCustomType;
|
|
@@ -2648,7 +3219,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2648
3219
|
}
|
|
2649
3220
|
history.push(projected);
|
|
2650
3221
|
}
|
|
2651
|
-
const projectedPending = pending.map(piShapeOfPending);
|
|
3222
|
+
const projectedPending = await Promise.all(pending.map((message) => piShapeOfPending(ctx, message)));
|
|
2652
3223
|
const event = {
|
|
2653
3224
|
type: "context",
|
|
2654
3225
|
messages: [...history, ...projectedPending]
|
|
@@ -2695,7 +3266,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
|
|
|
2695
3266
|
return rebuilt;
|
|
2696
3267
|
}
|
|
2697
3268
|
async function collectPiImages(ctx, messages) {
|
|
2698
|
-
const attachments = ctx
|
|
3269
|
+
const attachments = optionalService(ctx, "attachments");
|
|
2699
3270
|
if (attachments === void 0) return [];
|
|
2700
3271
|
const images = [];
|
|
2701
3272
|
for (const message of messages) {
|
|
@@ -2919,7 +3490,13 @@ function registerTool(ctx, state, tool) {
|
|
|
2919
3490
|
const mutated = state.argMutations.get(exec);
|
|
2920
3491
|
if (mutated !== void 0) state.argMutations.delete(exec);
|
|
2921
3492
|
const effective = mutated ?? args;
|
|
2922
|
-
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
|
+
});
|
|
2923
3500
|
const agent = exec.agent;
|
|
2924
3501
|
const result = await normalizeToolResultForDsh(ctx, await state.agentScope.run(agent, () => live.execute(String(exec.callId), prepared, exec.signal, (update) => {
|
|
2925
3502
|
dispatch(state, "tool_execution_update", {
|
|
@@ -2957,10 +3534,57 @@ function currentAgent(state) {
|
|
|
2957
3534
|
function toolRuntime(ctx, agent) {
|
|
2958
3535
|
return (agent?.ctx)?.tools ?? ctx.tools;
|
|
2959
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
|
+
}
|
|
2960
3565
|
function getActiveTools(ctx, state) {
|
|
2961
3566
|
const agent = currentAgent(state);
|
|
2962
3567
|
return toolRuntime(ctx, agent).schemas(agent).map((tool) => tool.name);
|
|
2963
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
|
+
*/
|
|
2964
3588
|
function setActiveTools(ctx, state, names) {
|
|
2965
3589
|
const unique = [...new Set(names)];
|
|
2966
3590
|
const agent = currentAgent(state);
|
|
@@ -2971,27 +3595,104 @@ function setActiveTools(ctx, state, names) {
|
|
|
2971
3595
|
logger(ctx).warn("[pi2dsh] setActiveTools deferred: the current agent exposes no scoped tools.restrict()");
|
|
2972
3596
|
return;
|
|
2973
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
|
+
}
|
|
2974
3604
|
state.toolRestrictions.get(agent)?.();
|
|
2975
|
-
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
|
+
}
|
|
2976
3622
|
}
|
|
2977
3623
|
function deliverAgentMessage(agent, message, mode) {
|
|
2978
3624
|
const deliver = agent[mode];
|
|
2979
3625
|
if (typeof deliver !== "function") throw new Error(`pi2dsh: active DSH agent has no ${mode}() delivery method`);
|
|
2980
3626
|
deliver.call(agent, message);
|
|
2981
3627
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
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) {
|
|
2984
3652
|
const blocks = await piToDshContent(ctx, typeof content === "string" ? [{
|
|
2985
3653
|
type: "text",
|
|
2986
3654
|
text: content
|
|
2987
3655
|
}] : content);
|
|
2988
|
-
|
|
3656
|
+
const message = createUserMessage({
|
|
2989
3657
|
content: blocks,
|
|
2990
3658
|
source: {
|
|
2991
3659
|
kind: "plugin",
|
|
2992
|
-
plugin: state.messageSource
|
|
3660
|
+
plugin: state.messageSource,
|
|
3661
|
+
...customType === void 0 ? {} : { piCustomType: customType }
|
|
2993
3662
|
}
|
|
2994
|
-
})
|
|
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" });
|
|
2995
3696
|
}
|
|
2996
3697
|
function combineExecSignal(options) {
|
|
2997
3698
|
const controller = new AbortController();
|
|
@@ -3101,6 +3802,51 @@ function registerCommand(ctx, state, command) {
|
|
|
3101
3802
|
}
|
|
3102
3803
|
logger(ctx).warn(`[pi2dsh] command /${command.name} was not registered: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
3103
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
|
+
}
|
|
3104
3850
|
function requireSession(state, operation) {
|
|
3105
3851
|
const session = agentSession(currentAgent(state));
|
|
3106
3852
|
if (session === void 0) throw new Error(`pi2dsh: ${operation} requires one active DSH agent with a durable session`);
|
|
@@ -3149,7 +3895,8 @@ function createPiApi(ctx, state) {
|
|
|
3149
3895
|
providerConfig: value,
|
|
3150
3896
|
store: oauthStoreOf(state)
|
|
3151
3897
|
}),
|
|
3152
|
-
warn: (message) => logger(ctx).warn(message)
|
|
3898
|
+
warn: (message) => logger(ctx).warn(message),
|
|
3899
|
+
resolveAttachments: () => optionalService(ctx, "attachments")
|
|
3153
3900
|
}
|
|
3154
3901
|
});
|
|
3155
3902
|
if (routeDisposer !== void 0) {
|
|
@@ -3204,8 +3951,7 @@ function createPiApi(ctx, state) {
|
|
|
3204
3951
|
},
|
|
3205
3952
|
sendMessage(message, options = {}) {
|
|
3206
3953
|
requireAgent(state, "sendMessage");
|
|
3207
|
-
|
|
3208
|
-
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);
|
|
3209
3955
|
},
|
|
3210
3956
|
sendUserMessage(content, options = {}) {
|
|
3211
3957
|
requireAgent(state, "sendUserMessage");
|
|
@@ -3217,15 +3963,17 @@ function createPiApi(ctx, state) {
|
|
|
3217
3963
|
},
|
|
3218
3964
|
setSessionName(name) {
|
|
3219
3965
|
const session = requireSession(state, "setSessionName");
|
|
3220
|
-
|
|
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));
|
|
3221
3969
|
dispatch(state, "session_info_changed", {
|
|
3222
3970
|
type: "session_info_changed",
|
|
3223
|
-
name: state
|
|
3971
|
+
name: sessionNameOf(ctx, state, session)
|
|
3224
3972
|
}, contextFor(ctx, state, currentAgent(state), void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] session_info_changed handler failed: ${String(error)}`));
|
|
3225
3973
|
},
|
|
3226
3974
|
getSessionName() {
|
|
3227
3975
|
const session = agentSession(currentAgent(state));
|
|
3228
|
-
return session === void 0 ? void 0 : state
|
|
3976
|
+
return session === void 0 ? void 0 : sessionNameOf(ctx, state, session);
|
|
3229
3977
|
},
|
|
3230
3978
|
setLabel(entryId, label) {
|
|
3231
3979
|
const session = requireSession(state, "setLabel");
|
|
@@ -3272,11 +4020,12 @@ function createPiApi(ctx, state) {
|
|
|
3272
4020
|
...typeof model?.id === "string" ? { model: model.id } : {}
|
|
3273
4021
|
};
|
|
3274
4022
|
if (override.model === void 0) return false;
|
|
4023
|
+
const previousModel = currentPiModel(state, agent);
|
|
3275
4024
|
state.modelOverrides.set(agent, override);
|
|
3276
4025
|
dispatch(state, "model_select", {
|
|
3277
4026
|
type: "model_select",
|
|
3278
4027
|
model,
|
|
3279
|
-
previousModel
|
|
4028
|
+
previousModel,
|
|
3280
4029
|
source: "set"
|
|
3281
4030
|
}, contextFor(ctx, state, agent, void 0)).catch((error) => logger(ctx).warn(`[pi2dsh] model_select handler failed: ${String(error)}`));
|
|
3282
4031
|
return true;
|
|
@@ -3474,6 +4223,12 @@ async function applyPiPackage(ctx, options) {
|
|
|
3474
4223
|
modelOverrides: /* @__PURE__ */ new WeakMap(),
|
|
3475
4224
|
thinkingLevels: /* @__PURE__ */ new WeakMap(),
|
|
3476
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(),
|
|
3477
4232
|
globalThinkingLevel: "off",
|
|
3478
4233
|
argMutations: /* @__PURE__ */ new WeakMap(),
|
|
3479
4234
|
streamingTexts: /* @__PURE__ */ new Map(),
|
|
@@ -3534,7 +4289,7 @@ async function applyPiPackage(ctx, options) {
|
|
|
3534
4289
|
},
|
|
3535
4290
|
piContentToDsh: (content) => piToDshContent(ctx, content),
|
|
3536
4291
|
deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
|
|
3537
|
-
messageFromSessionEvent,
|
|
4292
|
+
messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
|
|
3538
4293
|
messageSource: state.messageSource,
|
|
3539
4294
|
packageName: state.packageName
|
|
3540
4295
|
}, subagentOptions));
|
|
@@ -3570,7 +4325,10 @@ async function applyPiPackage(ctx, options) {
|
|
|
3570
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}`);
|
|
3571
4326
|
}
|
|
3572
4327
|
const runtimeInternals = {
|
|
4328
|
+
compactionReason,
|
|
4329
|
+
dshToPiContent,
|
|
3573
4330
|
expandPrompt,
|
|
4331
|
+
isSubagentOrigin,
|
|
3574
4332
|
normalizeToolResult,
|
|
3575
4333
|
splitArguments,
|
|
3576
4334
|
textBlocks
|
|
@@ -3578,4 +4336,4 @@ const runtimeInternals = {
|
|
|
3578
4336
|
//#endregion
|
|
3579
4337
|
export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
|
|
3580
4338
|
|
|
3581
|
-
//# sourceMappingURL=runtime-
|
|
4339
|
+
//# sourceMappingURL=runtime-DsPaznXJ.mjs.map
|