pi2dsh 0.25.1 → 0.26.0

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.
@@ -17,7 +17,7 @@ import { EventEmitter } from "node:events";
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
18
18
  import { createJiti } from "jiti";
19
19
  import * as llm from "@deepseek-ai/dsh-llm";
20
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
20
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ToolCallId, createToolResultMessage, createUserMessage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
21
21
  import { scopeOf } from "@deepseek-ai/dsh-scope";
22
22
  import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
23
23
  import { foldSurface } from "@deepseek-ai/dsh-session";
@@ -626,7 +626,6 @@ var PiSessionBridge = class {
626
626
  });
627
627
  } else if (type === "tool/result") {
628
628
  const message = data.message ?? {};
629
- const tool = (Array.isArray(message.content) ? message.content : []).find((block) => block.type === "tool-result");
630
629
  merged.push({
631
630
  time,
632
631
  entry: {
@@ -635,9 +634,9 @@ var PiSessionBridge = class {
635
634
  timestamp: new Date(time).toISOString(),
636
635
  message: {
637
636
  role: "toolResult",
638
- toolCallId: tool?.toolCallId,
639
- content: dshToPiContent$1(tool?.content ?? []),
640
- isError: tool?.isError === true
637
+ toolCallId: message.toolCallId,
638
+ content: dshToPiContent$1(message.content ?? []),
639
+ isError: message.isError === true
641
640
  }
642
641
  }
643
642
  });
@@ -834,6 +833,9 @@ var PiBridgedAgentSession = class {
834
833
  #aborted = false;
835
834
  #liveStreamAttempt;
836
835
  #streamingText = "";
836
+ #turnAssistant;
837
+ #pendingPrompt;
838
+ #promptActive = false;
837
839
  constructor(host, handle, tools, thinkingLevel) {
838
840
  this.#thinkingLevel = typeof thinkingLevel === "string" && thinkingLevel.length > 0 ? thinkingLevel : "off";
839
841
  this.#host = host;
@@ -962,35 +964,30 @@ var PiBridgedAgentSession = class {
962
964
  });
963
965
  }
964
966
  if (type === "request/header") {
965
- const config = event.data.config ?? {};
967
+ const config = event.data.header?.config ?? {};
966
968
  if (typeof config.model === "string") this.#model = {
967
969
  id: config.model,
968
970
  provider: String(config.provider ?? "")
969
971
  };
970
972
  }
971
973
  if (type === "tools/result" || type === "tool/result") {
972
- const data = event.data;
973
- const message = data.message;
974
- const blocks = Array.isArray(message?.content) ? message.content : [];
975
- for (const block of blocks) {
976
- const record = block;
977
- if (record?.type !== "tool-result") continue;
978
- const callId = String(record.toolCallId ?? (message?.source)?.callId ?? data.callId ?? "");
974
+ const message = event.data.message;
975
+ if (message?.role === "tool") {
976
+ const callId = String(message.toolCallId ?? "");
979
977
  this.#pendingToolCalls.delete(callId);
980
- const isError = record.isError === true;
978
+ const isError = message.isError === true;
981
979
  emit({
982
980
  type: "tool_execution_end",
983
981
  toolCallId: callId,
984
982
  toolName: this.#toolCallNames.get(callId) ?? "",
985
983
  result: {
986
- content: record.content ?? [],
984
+ content: message.content ?? [],
987
985
  isError
988
986
  },
989
987
  isError
990
988
  });
991
989
  this.#toolCallNames.delete(callId);
992
990
  }
993
- if (blocks.length === 0) this.#pendingToolCalls.delete(String(data.callId ?? ""));
994
991
  }
995
992
  if (type === "tool/call") {
996
993
  const data = event.data;
@@ -1037,6 +1034,10 @@ var PiBridgedAgentSession = class {
1037
1034
  if (message === void 0) return;
1038
1035
  if ((event.data?.source)?.form === "snapshot") this.#carried.add(message);
1039
1036
  this.messages.push(message);
1037
+ if (type === "assistant/message") this.#turnAssistant = {
1038
+ turn: Number(event.data.turn),
1039
+ message
1040
+ };
1040
1041
  emit({
1041
1042
  type: "message_start",
1042
1043
  message
@@ -1051,15 +1052,51 @@ var PiBridgedAgentSession = class {
1051
1052
  this.#streaming = false;
1052
1053
  this.#pendingToolCalls.clear();
1053
1054
  this.#messageProjection = this.#messageProjection.then(() => {
1055
+ const data = event.data;
1056
+ const turn = Number(data.turn);
1057
+ const reason = data.reason;
1058
+ const stopReason = reason?.kind === "error" || reason?.kind === "blocked" ? "error" : reason?.kind === "aborted" ? "aborted" : reason?.kind === "max-tokens" ? "length" : "stop";
1059
+ let message = this.#turnAssistant?.turn === turn ? this.#turnAssistant.message : void 0;
1060
+ const synthetic = message === void 0 && stopReason !== "stop";
1061
+ if (message === void 0 && stopReason !== "stop") {
1062
+ message = {
1063
+ role: "assistant",
1064
+ content: [],
1065
+ timestamp: Date.now()
1066
+ };
1067
+ this.messages.push(message);
1068
+ }
1069
+ if (message !== void 0) {
1070
+ message.stopReason = stopReason;
1071
+ if (stopReason === "error") {
1072
+ const failure = reason?.error;
1073
+ message.errorMessage = reason?.kind === "blocked" ? "DSH blocked the child turn before completion" : String(failure?.message ?? "DSH child turn failed");
1074
+ }
1075
+ if (synthetic) {
1076
+ emit({
1077
+ type: "message_start",
1078
+ message
1079
+ });
1080
+ emit({
1081
+ type: "message_end",
1082
+ message
1083
+ });
1084
+ }
1085
+ }
1054
1086
  emit({
1055
1087
  type: "turn_end",
1056
- turnIndex: Number(event.data.turn ?? 1) - 1,
1057
- message: {
1088
+ turnIndex: turn - 1,
1089
+ message: message ?? {
1058
1090
  role: "assistant",
1059
- content: []
1091
+ content: [],
1092
+ stopReason
1060
1093
  },
1061
1094
  toolResults: []
1062
1095
  });
1096
+ emit({
1097
+ type: "agent_end",
1098
+ messages: [...this.messages]
1099
+ });
1063
1100
  });
1064
1101
  }
1065
1102
  }
@@ -1068,6 +1105,15 @@ var PiBridgedAgentSession = class {
1068
1105
  return () => this.#subscribers.delete(handler);
1069
1106
  }
1070
1107
  async prompt(text) {
1108
+ if (this.#promptActive) throw new Error("pi2dsh: this child session is already processing a prompt");
1109
+ this.#promptActive = true;
1110
+ try {
1111
+ await this.#runPrompt(text);
1112
+ } finally {
1113
+ this.#promptActive = false;
1114
+ }
1115
+ }
1116
+ async #runPrompt(text) {
1071
1117
  this.#aborted = false;
1072
1118
  if (this.#seed.length > 0) {
1073
1119
  const seeded = this.#seed;
@@ -1085,7 +1131,7 @@ var PiBridgedAgentSession = class {
1085
1131
  this.#host.deliver(this.#handle.agent, createUserMessage({
1086
1132
  content: seedBlocks,
1087
1133
  source: {
1088
- kind: "plugin",
1134
+ kind: "pi2dsh",
1089
1135
  plugin: this.#host.messageSource
1090
1136
  }
1091
1137
  }), "inject");
@@ -1096,26 +1142,43 @@ var PiBridgedAgentSession = class {
1096
1142
  text: String(text)
1097
1143
  }]);
1098
1144
  const cordis = unscopedEventContext(this.#host.cordis);
1099
- const completed = new Promise((resolve) => {
1100
- const off = cordis.on("session/event", (session, event) => {
1101
- if (session !== this.#session) return;
1102
- if (event.type === "turn/end") {
1103
- off();
1104
- resolve();
1105
- }
1106
- });
1107
- this.#disposers.push(off);
1108
- });
1109
- this.#host.deliver(this.#handle.agent, createUserMessage({
1145
+ const message = createUserMessage({
1110
1146
  content: blocks,
1111
1147
  source: {
1112
- kind: "plugin",
1148
+ kind: "pi2dsh",
1113
1149
  plugin: this.#host.messageSource,
1114
1150
  form: "relay"
1115
1151
  }
1116
- }), "followup");
1117
- await completed;
1118
- await this.#messageProjection;
1152
+ });
1153
+ let turn;
1154
+ const offs = [];
1155
+ const cleanup = () => {
1156
+ for (const off of offs.splice(0)) off();
1157
+ };
1158
+ const completed = new Promise((resolve, reject) => {
1159
+ this.#pendingPrompt = {
1160
+ reject,
1161
+ dispose: cleanup
1162
+ };
1163
+ offs.push(cordis.on("agent/inbox/claimed", (payload) => {
1164
+ if (payload.agent === this.#handle.agent && payload.message?.id === message.id) turn = Number(payload.turn);
1165
+ }));
1166
+ offs.push(cordis.on("agent/disposed", (payload) => {
1167
+ if (payload.agent === this.#handle.agent) reject(/* @__PURE__ */ new Error("pi2dsh: child agent was disposed before its prompt completed"));
1168
+ }));
1169
+ offs.push(cordis.on("session/event", (session, event) => {
1170
+ if (session !== this.#session) return;
1171
+ if (event.type === "turn/end" && turn !== void 0 && Number(event.data.turn) === turn) resolve();
1172
+ }));
1173
+ });
1174
+ try {
1175
+ this.#host.deliver(this.#handle.agent, message, "followup");
1176
+ await completed;
1177
+ await this.#messageProjection;
1178
+ } finally {
1179
+ cleanup();
1180
+ this.#pendingPrompt = void 0;
1181
+ }
1119
1182
  }
1120
1183
  async steer(text, images) {
1121
1184
  await this.#queueMessage(text, images, "steer");
@@ -1133,7 +1196,7 @@ var PiBridgedAgentSession = class {
1133
1196
  this.#host.deliver(this.#handle.agent, createUserMessage({
1134
1197
  content: blocks,
1135
1198
  source: {
1136
- kind: "plugin",
1199
+ kind: "pi2dsh",
1137
1200
  plugin: this.#host.messageSource
1138
1201
  }
1139
1202
  }), mode);
@@ -1231,6 +1294,8 @@ var PiBridgedAgentSession = class {
1231
1294
  } catch {}
1232
1295
  }
1233
1296
  async dispose() {
1297
+ this.#pendingPrompt?.reject(/* @__PURE__ */ new Error("pi2dsh: child session was disposed before its prompt completed"));
1298
+ this.#pendingPrompt?.dispose();
1234
1299
  for (const dispose of this.#disposers.splice(0)) try {
1235
1300
  dispose();
1236
1301
  } catch {}
@@ -2403,7 +2468,7 @@ function dshBlocksFromPiContent(content) {
2403
2468
  }
2404
2469
  /**
2405
2470
  * Pi request context → DSH GenerateOptions parts. System prompt moves to the
2406
- * dedicated slot; toolResult messages become DSH tool-result user messages.
2471
+ * dedicated slot; toolResult messages become native DSH tool-role messages.
2407
2472
  */
2408
2473
  function piRequestToDshMessages(context) {
2409
2474
  const messages = [];
@@ -2422,19 +2487,11 @@ function piRequestToDshMessages(context) {
2422
2487
  model: String(message.model ?? context.model ?? "unknown")
2423
2488
  }
2424
2489
  });
2425
- else if (message.role === "toolResult") messages.push({
2426
- role: "user",
2427
- content: [{
2428
- type: "tool-result",
2429
- toolCallId: String(message.toolCallId ?? ""),
2430
- content: dshBlocksFromPiContent(message.content),
2431
- isError: message.isError === true
2432
- }],
2433
- source: {
2434
- kind: "tool",
2435
- callId: String(message.toolCallId ?? "")
2436
- }
2437
- });
2490
+ else if (message.role === "toolResult") messages.push(createToolResultMessage({
2491
+ callId: ToolCallId(String(message.toolCallId ?? "")),
2492
+ content: dshBlocksFromPiContent(message.content),
2493
+ isError: message.isError === true
2494
+ }));
2438
2495
  const system = typeof context.systemPrompt === "string" && context.systemPrompt.length > 0 ? context.systemPrompt : void 0;
2439
2496
  return {
2440
2497
  ...system === void 0 ? {} : { system },
@@ -2712,18 +2769,17 @@ async function dshRequestToPiContext(options, attachments) {
2712
2769
  }
2713
2770
  for (const message of history) {
2714
2771
  const content = Array.isArray(message.content) ? message.content : [];
2715
- const source = message.source;
2716
2772
  if (message.role === "system") throw new Error("pi2dsh: this Pi provider cannot represent an in-history system prompt update");
2717
- else if (message.role === "user" && source?.kind === "tool") {
2718
- const result = content[0];
2719
- const toolCallId = String(result?.toolCallId ?? "");
2773
+ else if (message.role === "developer") throw new LlmError("pi2dsh: this Pi provider cannot represent developer messages", "UNSUPPORTED_CONTENT");
2774
+ else if (message.role === "tool") {
2775
+ const toolCallId = String(message.toolCallId ?? "");
2720
2776
  const toolName = toolNamesById.get(toolCallId);
2721
2777
  piMessages.push({
2722
2778
  role: "toolResult",
2723
2779
  toolCallId,
2724
2780
  ...toolName === void 0 ? {} : { toolName },
2725
- content: await piContentFromDshBlocks(Array.isArray(result?.content) ? result.content : [], attachments),
2726
- isError: result?.isError === true
2781
+ content: await piContentFromDshBlocks(content, attachments),
2782
+ isError: message.isError === true
2727
2783
  });
2728
2784
  } else if (message.role === "user") piMessages.push({
2729
2785
  role: "user",
@@ -3310,6 +3366,7 @@ const DSH_THINKING_FORMATS = /* @__PURE__ */ new Set([
3310
3366
  "deepseek",
3311
3367
  "openrouter",
3312
3368
  "together",
3369
+ "baseten",
3313
3370
  "zai",
3314
3371
  "qwen",
3315
3372
  "chat-template",
@@ -3321,6 +3378,7 @@ const DSH_MAX_TOKENS_FIELDS = /* @__PURE__ */ new Set(["max_completion_tokens",
3321
3378
  const DSH_CACHE_CONTROL_FORMATS = /* @__PURE__ */ new Set(["anthropic"]);
3322
3379
  const DSH_CHAT_TEMPLATE_VARS = /* @__PURE__ */ new Set(["thinking.enabled", "thinking.effort"]);
3323
3380
  const COMPLETIONS = /* @__PURE__ */ new Set(["openai-completions"]);
3381
+ const RESPONSES = /* @__PURE__ */ new Set(["openai-responses"]);
3324
3382
  const ANTHROPIC = /* @__PURE__ */ new Set(["anthropic-messages"]);
3325
3383
  const COMPLETIONS_AND_RESPONSES = /* @__PURE__ */ new Set(["openai-completions", "openai-responses"]);
3326
3384
  const DSH_COMPAT_FIELD_PROTOCOLS = {
@@ -3331,6 +3389,9 @@ const DSH_COMPAT_FIELD_PROTOCOLS = {
3331
3389
  supportsFinishReason: COMPLETIONS,
3332
3390
  chatTemplateArgs: COMPLETIONS,
3333
3391
  supportsThinkingTokenBudget: COMPLETIONS,
3392
+ thinkingTokenBudgetField: COMPLETIONS,
3393
+ vllmPriority: COMPLETIONS,
3394
+ supportsMaxOutputTokens: RESPONSES,
3334
3395
  maxTokensField: COMPLETIONS,
3335
3396
  requiresToolResultName: COMPLETIONS,
3336
3397
  requiresAssistantAfterToolResult: COMPLETIONS,
@@ -3385,9 +3446,13 @@ function compatProfileOf(value, api) {
3385
3446
  const candidate = compat[field];
3386
3447
  if (field === "maxTokensField") {
3387
3448
  if (typeof candidate === "string" && DSH_MAX_TOKENS_FIELDS.has(candidate)) output[field] = candidate;
3449
+ } else if (field === "vllmPriority") {
3450
+ if (typeof candidate === "number" && Number.isSafeInteger(candidate)) output[field] = candidate;
3451
+ } else if (field === "thinkingTokenBudgetField") {
3452
+ if (candidate === "thinking_token_budget" || candidate === "thinking_budget" || candidate === "thinking_budget_tokens") output[field] = candidate;
3388
3453
  } else if (field === "thinkingFormat") {
3389
3454
  if (typeof candidate === "string" && DSH_THINKING_FORMATS.has(candidate)) output[field] = candidate;
3390
- } else if (field === "chatTemplateKwargs") {
3455
+ } else if (field === "chatTemplateKwargs" || field === "chatTemplateArgs") {
3391
3456
  const kwargs = chatTemplateKwargsOf(candidate);
3392
3457
  if (kwargs !== void 0) output[field] = kwargs;
3393
3458
  } else if (field === "cacheControlFormat") {
@@ -4087,12 +4152,16 @@ async function createPiTuiExtensionSurfaceAdapter(ctx, packageName, instanceKey)
4087
4152
  const extensionService = service(ctx);
4088
4153
  if (extensionService === void 0) return void 0;
4089
4154
  const api = extensionService.api();
4090
- if (api.apiVersion !== 1 || !api.capabilities.has("unstable.surface.handle")) {
4155
+ if (api.apiVersion !== 2 || !api.capabilities.has("unstable.surface.handle")) {
4091
4156
  ctx.logger?.warn(`pi2dsh: piTuiExtensions cannot host Pi components (api=${api.apiVersion}, raw surface=${api.capabilities.has("unstable.surface.handle")})`);
4092
4157
  return;
4093
4158
  }
4094
4159
  try {
4095
- const [hostModule, unstableModule] = await Promise.all([import("@xmoon76/dsh-pi-tui"), import("@xmoon76/dsh-pi-tui/extensions/unstable")]);
4160
+ const hostEntry = "@xmoon76/dsh-pi-tui";
4161
+ const unstableEntry = "@xmoon76/dsh-pi-tui/extensions/unstable";
4162
+ const loader = ctx.get("loader", false);
4163
+ const importPublic = (name) => loader === void 0 ? import(name) : loader.import(name);
4164
+ const [hostModule, unstableModule] = await Promise.all([importPublic(hostEntry), importPublic(unstableEntry)]);
4096
4165
  if (unstableModule.UNSTABLE_API_LEVEL !== 1) {
4097
4166
  ctx.logger?.warn(`pi2dsh: unsupported dsh-pi-tui unstable API level ${unstableModule.UNSTABLE_API_LEVEL}`);
4098
4167
  return;
@@ -4744,21 +4813,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4744
4813
  ...auth.baseUrl === void 0 && config.baseUrl === void 0 ? {} : { baseUrl: auth.baseUrl ?? config.baseUrl }
4745
4814
  };
4746
4815
  }
4747
- try {
4748
- const entry = llmOf(ctx)?.listConfigurableProviders?.()?.find((candidate) => candidate.provider === provider);
4749
- if (entry === void 0) return { ok: false };
4750
- const section = ctx.get("settings")?.get(entry.settingsNs);
4751
- const ref = entry.settingsPath.reduce((node, key) => typeof node === "object" && node !== null ? node[key] : void 0, section)?.apiKeyEnv;
4752
- if (typeof ref !== "string" || ref.length === 0) return { ok: false };
4753
- const resolved = await ctx.get("credentials")?.resolve(ref);
4754
- if (typeof resolved?.value !== "string" || resolved.value.length === 0) return { ok: false };
4755
- return {
4756
- ok: true,
4757
- apiKey: resolved.value
4758
- };
4759
- } catch {
4760
- return { ok: false };
4761
- }
4816
+ return resolveHostModelAuth(ctx, provider);
4762
4817
  },
4763
4818
  getProviderAuthStatus: (provider) => providerSupportsOAuth(providerConfig(provider)) ? "oauth" : "none",
4764
4819
  getProvider: (provider) => {
@@ -4774,7 +4829,13 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4774
4829
  if (result instanceof Error) throw result;
4775
4830
  return result;
4776
4831
  },
4777
- getRegisteredProviderConfig: (provider) => providerConfig(provider),
4832
+ registerProvider: (providerOrName, config) => {
4833
+ if (typeof providerOrName === "string" && config === void 0) throw new Error("Provider config is required when registering by name");
4834
+ registerPiProvider(ctx, state, providerOrName, config);
4835
+ },
4836
+ unregisterProvider: (provider) => unregisterPiProvider(ctx, state, provider),
4837
+ getRegisteredProviderConfig: (provider) => state.nativeProviders.has(provider) ? void 0 : providerConfig(provider),
4838
+ getRegisteredNativeProvider: (provider) => state.nativeProviders.has(provider) ? providerConfig(provider) : void 0,
4778
4839
  getRegisteredProviderIds: () => [...state.providers.keys()],
4779
4840
  getProviderDisplayName: (provider) => {
4780
4841
  const config = providerConfig(provider);
@@ -4822,7 +4883,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4822
4883
  const base = {
4823
4884
  ...sessionOverride === void 0 ? {} : {
4824
4885
  sendMessage: (message, options = {}) => sendPiMessage(ctx, state, message.content, deliveryMode(options), sessionOverride, typeof message.customType === "string" ? message.customType : void 0),
4825
- sendUserMessage: (content) => sendPiMessage(ctx, state, content, "followup", sessionOverride),
4886
+ sendUserMessage: (content) => reportUnawaitedDelivery(ctx, state, "sendUserMessage", sendPiMessage(ctx, state, content, "followup", sessionOverride)),
4826
4887
  appendEntry: (customType, data) => {
4827
4888
  state.bridge.appendCustomEntry(String(sessionOverride.id ?? ""), customType, data);
4828
4889
  }
@@ -5210,12 +5271,12 @@ async function messageFromSessionEvent(ctx, event) {
5210
5271
  };
5211
5272
  }
5212
5273
  if (type === "tool/result") {
5213
- const tool = (record.message?.content ?? []).find((block) => block.type === "tool-result");
5274
+ const message = record.message;
5214
5275
  return {
5215
5276
  role: "toolResult",
5216
- toolCallId: tool?.toolCallId,
5217
- content: await dshToPiContent(ctx, tool?.content ?? []),
5218
- isError: tool?.isError === true
5277
+ toolCallId: message?.toolCallId,
5278
+ content: await dshToPiContent(ctx, message?.content ?? []),
5279
+ isError: message?.isError === true
5219
5280
  };
5220
5281
  }
5221
5282
  }
@@ -5225,12 +5286,9 @@ function sourceReason(value) {
5225
5286
  /**
5226
5287
  * Claim Pi's once-per-session `session_start` event.
5227
5288
  *
5228
- * dsh-TUI can execute a slash command before DSH starts the first model turn,
5229
- * so `agent/session-start` has not necessarily been announced yet. Pi, by
5230
- * contrast, announces `session_start` before the user can execute a command.
5231
- * The command bridge uses this same claim before invoking a Pi command; when
5232
- * DSH later announces the real agent lifecycle, the durable session id keeps
5233
- * the second path from restarting the extension.
5289
+ * Normal engine mounts complete inside DSH's serial agent/created event.
5290
+ * The same idempotent claim also protects direct runtime users and reloads:
5291
+ * command/tool entry must not restart an already initialized Pi extension.
5234
5292
  */
5235
5293
  function claimPiSessionStart(state, agent) {
5236
5294
  const id = agentSession(agent)?.id;
@@ -5356,7 +5414,7 @@ async function startPiSession(ctx, state, agent, reason, signal) {
5356
5414
  function subscribeLifecycle(ctx, state) {
5357
5415
  const cordis = ctx;
5358
5416
  const warn = (event, error) => logger(ctx).warn(`[pi2dsh] ${event} handler failed: ${String(error)}`);
5359
- cordis.on("agent/session-start", (payload) => {
5417
+ cordis.on("agent/created", async (payload) => {
5360
5418
  const agent = payload.agent;
5361
5419
  if (!acceptsAgent(state, agent)) return;
5362
5420
  state.activeAgents.add(agent);
@@ -5368,7 +5426,7 @@ function subscribeLifecycle(ctx, state) {
5368
5426
  state.pendingSessionStarts.set(agent, sourceReason(payload.source));
5369
5427
  return;
5370
5428
  }
5371
- startPiSession(ctx, state, agent, sourceReason(payload.source), void 0).catch((error) => warn("session_start", error));
5429
+ await startPiSession(ctx, state, agent, sourceReason(payload.source), payload.signal);
5372
5430
  });
5373
5431
  cordis.on("agent/disposed", (payload) => {
5374
5432
  const agent = payload.agent;
@@ -5662,7 +5720,7 @@ async function flushPendingSessionStarts(ctx, state) {
5662
5720
  * session guard must deliberately NOT suppress this replay: from the new
5663
5721
  * instance's point of view this is its first `session_start`. The command's
5664
5722
  * AsyncLocalStorage agent is included because a lean composition can execute
5665
- * a Pi command without publishing DSH's optional agent/session-start event.
5723
+ * a Pi command through the runtime directly, outside engine-managed creation.
5666
5724
  */
5667
5725
  async function restartReloadedPiSessions(ctx, state) {
5668
5726
  state.extensionsReady = true;
@@ -6035,7 +6093,7 @@ async function runBeforeAgentStart(ctx, state, agent, signal, assembled) {
6035
6093
  injected.push(createUserMessage({
6036
6094
  content: blocks,
6037
6095
  source: {
6038
- kind: "plugin",
6096
+ kind: "pi2dsh",
6039
6097
  plugin: state.messageSource,
6040
6098
  ...typeof message.customType === "string" ? { piCustomType: message.customType } : {}
6041
6099
  }
@@ -6193,7 +6251,7 @@ async function applyPiContextTransform(ctx, state, agent, signal, pending) {
6193
6251
  rebuilt.push(createUserMessage({
6194
6252
  content: blocks.map((block) => block.type === "text" ? files.get(block.text) ?? block : block),
6195
6253
  source: original.source ?? {
6196
- kind: "plugin",
6254
+ kind: "pi2dsh",
6197
6255
  plugin: state.messageSource
6198
6256
  }
6199
6257
  }));
@@ -6541,6 +6599,7 @@ async function materializeAttachmentImage(ctx, attachment) {
6541
6599
  }
6542
6600
  function ensureLoginCommand(ctx, state) {
6543
6601
  if (!shouldRegisterCompatibilityCommand("login", state.tuiSurfaces?.nativeCommands) && optionalService(ctx, "authorization") !== void 0) {
6602
+ state.shared.retireLoginCommand?.();
6544
6603
  logger(ctx).debug("[pi2dsh] native terminal owns /login and the authorization service is composed; Pi OAuth flows are available through it");
6545
6604
  return;
6546
6605
  }
@@ -6548,8 +6607,26 @@ function ensureLoginCommand(ctx, state) {
6548
6607
  state.shared.loginCommandRegistered = true;
6549
6608
  try {
6550
6609
  registerLoginCommand(ctx, state);
6551
- ctx.effect(() => () => {
6552
- state.shared.loginCommandRegistered = false;
6610
+ const retire = () => {
6611
+ state.commandDisposers.get("login")?.();
6612
+ state.commandDisposers.delete("login");
6613
+ state.commands.delete("login");
6614
+ for (const [name, owner] of state.dshCommandOwners) if (owner === "login") state.dshCommandOwners.delete(name);
6615
+ if (state.shared.retireLoginCommand === retire) {
6616
+ delete state.shared.retireLoginCommand;
6617
+ state.shared.loginCommandRegistered = false;
6618
+ }
6619
+ };
6620
+ state.shared.retireLoginCommand = retire;
6621
+ ctx.effect(() => retire);
6622
+ ctx.inject(["piTuiExtensions", "authorization"], async (inner) => {
6623
+ const terminal = await createPiTuiExtensionSurfaceAdapter(inner, state.packageName);
6624
+ if (terminal === void 0) return;
6625
+ try {
6626
+ if (!shouldRegisterCompatibilityCommand("login", terminal.nativeCommands)) retire();
6627
+ } finally {
6628
+ terminal.dispose();
6629
+ }
6553
6630
  });
6554
6631
  } catch (error) {
6555
6632
  state.shared.loginCommandRegistered = false;
@@ -7230,6 +7307,20 @@ function deliverAgentMessage(agent, message, mode) {
7230
7307
  deliver.call(agent, message);
7231
7308
  }
7232
7309
  /**
7310
+ * Pi declares sendMessage / sendUserMessage as returning void, so packages call
7311
+ * them without awaiting. A delivery that fails — typically a completion
7312
+ * notice sent while the host is already tearing the agent down — would
7313
+ * otherwise surface as an unhandled rejection, which the DSH CLI reports as a
7314
+ * fatal failure and a non-zero exit. The failure is reported once, as a
7315
+ * warning naming the package; a caller that does await still receives it.
7316
+ */
7317
+ function reportUnawaitedDelivery(ctx, state, api, delivery) {
7318
+ delivery.catch((error) => {
7319
+ logger(ctx).warn(`[pi2dsh] ${state.packageName}: ${api} was not delivered: ${error instanceof Error ? error.message : String(error)}`);
7320
+ });
7321
+ return delivery;
7322
+ }
7323
+ /**
7233
7324
  * Pi's `sendMessage` / `sendUserMessage`, on DSH.
7234
7325
  *
7235
7326
  * Two things this has to get right that a plain `deliverAgentMessage` does not:
@@ -7260,7 +7351,7 @@ async function sendPiMessage(ctx, state, content, mode, sessionOverride, customT
7260
7351
  const message = createUserMessage({
7261
7352
  content: blocks,
7262
7353
  source: {
7263
- kind: "plugin",
7354
+ kind: "pi2dsh",
7264
7355
  plugin: state.messageSource,
7265
7356
  ...customType === void 0 ? { form: "relay" } : { piCustomType: customType }
7266
7357
  }
@@ -7612,6 +7703,44 @@ async function runPiPrintRequest(ctx, state, parent, request, signal) {
7612
7703
  await child.dispose();
7613
7704
  }
7614
7705
  }
7706
+ /**
7707
+ * The ONE provider registration path. Pi exposes it twice — `pi.registerProvider`
7708
+ * and `ctx.modelRegistry.registerProvider` (model-registry.ts) — over the same
7709
+ * runtime, so both surfaces land here. A string first argument is Pi's legacy
7710
+ * (name, config) form; an object is a pi-ai native Provider. Pi keeps the two in
7711
+ * separate ledgers that replace each other per id, which is what
7712
+ * getRegisteredProviderConfig / getRegisteredNativeProvider read back.
7713
+ */
7714
+ function registerPiProvider(ctx, state, providerOrName, config) {
7715
+ const name = typeof providerOrName === "string" ? providerOrName : String(providerOrName?.id ?? providerOrName?.name ?? "unnamed");
7716
+ const value = typeof providerOrName === "string" ? config ?? {} : providerOrName;
7717
+ state.providers.set(name, value);
7718
+ if (typeof providerOrName === "string") state.nativeProviders.delete(name);
7719
+ else state.nativeProviders.add(name);
7720
+ recordPackageProviderRegistration(state.shared, state.packageName, name, value);
7721
+ if (recomposeSharedProvider(state.shared, name)) retireSharedProviderRoute(state.shared, name);
7722
+ const canonical = state.shared.providers.get(name) ?? value;
7723
+ if (providerSupportsOAuth(canonical)) {
7724
+ ensureLoginCommand(ctx, state);
7725
+ maybeProjectAuthorizationFlow(ctx, state, name);
7726
+ logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} supports OAuth — log in with /login ${name}`);
7727
+ }
7728
+ if (registerSharedProviderRoute(ctx, state, name, canonical) !== void 0) {
7729
+ state.modelCatalog?.refresh();
7730
+ if (providerCarriesTransport(canonical)) logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
7731
+ } else if (!providerSupportsOAuth(canonical)) logger(ctx).info(`[pi2dsh] recorded Pi provider ${JSON.stringify(name)}; model calls stay on DSH llm adapters`);
7732
+ discoverProviderModels(ctx, state, name, canonical);
7733
+ }
7734
+ function unregisterPiProvider(ctx, state, name) {
7735
+ state.providers.delete(name);
7736
+ state.nativeProviders.delete(name);
7737
+ releaseSharedProviderRoute(state, name);
7738
+ if ((state.shared.providerPackages?.get(name))?.delete(state.packageName) === true && recomposeSharedProvider(state.shared, name)) {
7739
+ retireSharedProviderRoute(state.shared, name);
7740
+ const restored = state.shared.providers.get(name);
7741
+ if (restored !== void 0) registerSharedProviderRoute(ctx, state, name, restored);
7742
+ }
7743
+ }
7615
7744
  function createPiApi(ctx, state) {
7616
7745
  return {
7617
7746
  on(event, handler) {
@@ -7637,33 +7766,8 @@ function createPiApi(ctx, state) {
7637
7766
  logger(ctx).warn(`[pi2dsh] Pi flag --${name} uses its default only; DSH CLI registration is unsupported`);
7638
7767
  },
7639
7768
  getFlag: (name) => state.flags.get(name),
7640
- registerProvider(providerOrName, config) {
7641
- const name = typeof providerOrName === "string" ? providerOrName : String(providerOrName?.id ?? providerOrName?.name ?? "unnamed");
7642
- const value = typeof providerOrName === "string" ? config ?? {} : providerOrName;
7643
- state.providers.set(name, value);
7644
- recordPackageProviderRegistration(state.shared, state.packageName, name, value);
7645
- if (recomposeSharedProvider(state.shared, name)) retireSharedProviderRoute(state.shared, name);
7646
- const canonical = state.shared.providers.get(name) ?? value;
7647
- if (providerSupportsOAuth(canonical)) {
7648
- ensureLoginCommand(ctx, state);
7649
- maybeProjectAuthorizationFlow(ctx, state, name);
7650
- logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} supports OAuth — log in with /login ${name}`);
7651
- }
7652
- if (registerSharedProviderRoute(ctx, state, name, canonical) !== void 0) {
7653
- state.modelCatalog?.refresh();
7654
- if (providerCarriesTransport(canonical)) logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
7655
- } else if (!providerSupportsOAuth(canonical)) logger(ctx).info(`[pi2dsh] recorded Pi provider ${JSON.stringify(name)}; model calls stay on DSH llm adapters`);
7656
- discoverProviderModels(ctx, state, name, canonical);
7657
- },
7658
- unregisterProvider(name) {
7659
- state.providers.delete(name);
7660
- releaseSharedProviderRoute(state, name);
7661
- if ((state.shared.providerPackages?.get(name))?.delete(state.packageName) === true && recomposeSharedProvider(state.shared, name)) {
7662
- retireSharedProviderRoute(state.shared, name);
7663
- const restored = state.shared.providers.get(name);
7664
- if (restored !== void 0) registerSharedProviderRoute(ctx, state, name, restored);
7665
- }
7666
- },
7769
+ registerProvider: (providerOrName, config) => registerPiProvider(ctx, state, providerOrName, config),
7770
+ unregisterProvider: (name) => unregisterPiProvider(ctx, state, name),
7667
7771
  registerMessageRenderer(customType, renderer) {
7668
7772
  state.messageRenderers.set(customType, renderer);
7669
7773
  },
@@ -7675,11 +7779,11 @@ function createPiApi(ctx, state) {
7675
7779
  },
7676
7780
  sendMessage(message, options = {}) {
7677
7781
  requireAgent(state, "sendMessage");
7678
- return sendPiMessage(ctx, state, message.content, deliveryMode(options), void 0, typeof message.customType === "string" ? message.customType : void 0);
7782
+ return reportUnawaitedDelivery(ctx, state, "sendMessage", sendPiMessage(ctx, state, message.content, deliveryMode(options), void 0, typeof message.customType === "string" ? message.customType : void 0));
7679
7783
  },
7680
7784
  sendUserMessage(content, options = {}) {
7681
7785
  requireAgent(state, "sendUserMessage");
7682
- return sendPiMessage(ctx, state, content, options.deliverAs === "steer" ? "steer" : "followup");
7786
+ return reportUnawaitedDelivery(ctx, state, "sendUserMessage", sendPiMessage(ctx, state, content, options.deliverAs === "steer" ? "steer" : "followup"));
7683
7787
  },
7684
7788
  appendEntry(customType, data) {
7685
7789
  const session = requireSession(state, "appendEntry");
@@ -7849,7 +7953,7 @@ async function registerPromptCommands(ctx, state, rootDir, manifest) {
7849
7953
  text: expandPrompt(text, rawInput)
7850
7954
  }],
7851
7955
  source: {
7852
- kind: "plugin",
7956
+ kind: "pi2dsh",
7853
7957
  plugin: `pi2dsh:${manifest.package.name}`,
7854
7958
  form: "relay"
7855
7959
  }
@@ -8004,6 +8108,7 @@ async function applyPiPackage(ctx, options) {
8004
8108
  messageRenderers: /* @__PURE__ */ new Map(),
8005
8109
  entryRenderers: /* @__PURE__ */ new Map(),
8006
8110
  providers: /* @__PURE__ */ new Map(),
8111
+ nativeProviders: /* @__PURE__ */ new Set(),
8007
8112
  autocompleteProviders: [],
8008
8113
  editorBuffers: /* @__PURE__ */ new WeakMap(),
8009
8114
  toolsExpanded: false,
@@ -8490,7 +8595,28 @@ async function applyPiPackage(ctx, options) {
8490
8595
  const healthSuffix = health === void 0 || health.status === "ok" ? "" : ` — ${health.status.toUpperCase()}: missing Pi capabilities ${health.gaps.join(", ")}`;
8491
8596
  logger(ctx).info(`[pi2dsh] loaded ${options.manifest.package.name}: ${state.tools.size} tools, ${state.commands.size} commands, ${options.manifest.skillDirs.length} skill roots${healthSuffix}${state.hostAnchor ? " (host anchor)" : ""}`);
8492
8597
  }
8598
+ /** Resolve a host-owned route through its public config descriptor and credential reference. */
8599
+ async function resolveHostModelAuth(ctx, provider) {
8600
+ try {
8601
+ const entry = ctx.get("llm")?.listConfigurableProviders().find((candidate) => candidate.provider === provider);
8602
+ if (entry === void 0) return { ok: false };
8603
+ const section = ctx.get("settings")?.describe({ redactSecrets: true }).find((item) => item.ns === entry.settingsNs)?.value;
8604
+ const ref = entry.settingsPath.reduce((node, key) => typeof node === "object" && node !== null ? node[key] : void 0, section)?.apiKeyEnv;
8605
+ if (typeof ref !== "string" || ref.length === 0) return { ok: false };
8606
+ const resolved = await ctx.get("credentials")?.resolve(ref);
8607
+ return typeof resolved?.value === "string" && resolved.value.length > 0 ? {
8608
+ ok: true,
8609
+ apiKey: resolved.value
8610
+ } : { ok: false };
8611
+ } catch (error) {
8612
+ return {
8613
+ ok: false,
8614
+ error: `Host credential resolution failed: ${error instanceof Error ? error.message : String(error)}`
8615
+ };
8616
+ }
8617
+ }
8493
8618
  const runtimeInternals = {
8619
+ resolveHostModelAuth,
8494
8620
  compactionReason,
8495
8621
  resolveOfferedChoice,
8496
8622
  currentPiModel,
@@ -8509,4 +8635,4 @@ const runtimeInternals = {
8509
8635
  //#endregion
8510
8636
  export { overlayProviderConfig as a, runtimeInternals as c, normalizeToolSchema as i, getSharedChildExtensionCatalog as n, registerChildExtensionCatalog as o, mergeProviderRegistration as r, registerVisionCompanions as s, applyPiPackage as t };
8511
8637
 
8512
- //# sourceMappingURL=runtime-CCzYSxhI.mjs.map
8638
+ //# sourceMappingURL=runtime-BFJsumJX.mjs.map