@ycodium-ai/agent-pi 0.2.3598 → 0.2.3608

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +56 -21
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -21,8 +21,8 @@ in `/v1`, without `/chat/completions` or `/responses`. For Anthropic Messages,
21
21
  supply the service root or a base URL ending in `/v1`; the runtime resolves the
22
22
  request route to `/v1/messages`. URLs must not contain embedded credentials,
23
23
  query parameters or a fragment. The model ID must be accepted by that service. The instance exposes
24
- that one configured model; its readiness check does not make a model request or
25
- verify API credentials. The first request checks access to the service.
24
+ the configured model plus the instance's custom models, all on the same endpoint and key;
25
+ its readiness check does not make a model request or verify API credentials. The first request checks access to the service.
26
26
 
27
27
  ## Install
28
28
 
@@ -48,7 +48,8 @@ is shown as invalid and must be explicitly replaced. Advanced configurations
48
48
  can also supply `YCODIUM_PI_API_BASE_URL`.
49
49
 
50
50
  The plugin starts `pi --mode rpc` through Ycodium's managed process API. It
51
- provides temporary model and MCP configuration, and enables Pi's built-in MCP and tool-search
51
+ provides temporary model and MCP configuration (every listed model under one `ycodium`
52
+ provider), and enables Pi's built-in MCP and tool-search
52
53
  extensions. MCP tools are discovered on demand rather than all advertised in the first request.
53
54
  Native session files live in a directory specific
54
55
  to the instance under `~/.pi/ycodium/`, unless a session directory is configured
@@ -60,9 +61,11 @@ history and require a new thread. Missing saved history is reported as an error;
60
61
  does not silently replace it with an empty conversation.
61
62
 
62
63
  Pi runs with full process permissions. It provides no sandbox or native per-tool
63
- approval flow. Context compaction and steering use native RPC operations. This
64
+ approval flow. Context compaction and steering use native RPC operations. Choosing another listed model
65
+ sends `set_model` before the next prompt in the same process; steering keeps the current model.
66
+ Models are declared without reasoning support, so no thinking-level option is exposed. This
64
67
  integration does not offer OAuth, an auxiliary text-generation assistant,
65
- mid-session model switching, inline image attachments, or a guarantee that an
68
+ inline image attachments, or a guarantee that an
66
69
  interrupted tool's external side effects have stopped. MCP transports are stdio
67
70
  and HTTP; legacy SSE is unsupported.
68
71
 
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { definePlugin } from "@ycodium-ai/plugin-api";
5
5
  import * as NodeCrypto from "node:crypto";
6
6
  import * as NodeOS from "node:os";
7
7
  import * as NodePath from "node:path";
8
+ import { matchCatalogModel } from "@ycodium-ai/plugin-api/executor";
8
9
 
9
10
  // src/protocol.ts
10
11
  var PI_API_PROTOCOLS = [
@@ -37,11 +38,16 @@ function parseConfig(value, instanceId) {
37
38
  );
38
39
  const configuredHome = string(config.homePath).trim();
39
40
  const homePath = configuredHome.startsWith("~/") || configuredHome.startsWith("~\\") ? NodePath.join(NodeOS.homedir(), configuredHome.slice(2)) : configuredHome;
41
+ const model = string(config.model).trim();
42
+ const custom = (Array.isArray(config.customModels) ? config.customModels : []).map(
43
+ (entry) => string(typeof entry === "string" ? entry : record(entry).slug).trim()
44
+ );
40
45
  return {
41
46
  binaryPath: string(config.binaryPath).trim() || "pi",
42
47
  apiBaseUrl: string(config.apiBaseUrl).trim() || environment.YCODIUM_PI_API_BASE_URL?.trim() || "",
43
48
  apiProtocol: readPiApiProtocol(config.apiProtocol),
44
- model: string(config.model).trim(),
49
+ model,
50
+ models: [...new Set([model, ...custom].filter(Boolean))],
45
51
  environment,
46
52
  homePath: homePath ? NodePath.resolve(homePath) : NodePath.join(NodeOS.homedir(), ".pi", "ycodium", identifier(instanceId))
47
53
  };
@@ -65,6 +71,18 @@ function validateConnection(config) {
65
71
  if (!config.model) throw new Error("Pi model ID is required.");
66
72
  if (!config.environment[PI_API_KEY_ENV]?.trim()) throw new Error("Pi API key is required.");
67
73
  }
74
+ function requestedModel(config, selection) {
75
+ const raw = string(record(selection).model).trim();
76
+ if (!raw) return void 0;
77
+ const id = matchCatalogModel(raw, config.models) ?? matchCatalogModel(raw.replace(/^ycodium\//, ""), config.models);
78
+ if (id) return id;
79
+ throw Object.assign(
80
+ new Error(
81
+ `Pi instance does not list model "${raw}". Choose one of: ${config.models.join(", ")}. Provider setting changes apply after the session restarts.`
82
+ ),
83
+ { code: "provider-rejected" }
84
+ );
85
+ }
68
86
  function redact(config, value) {
69
87
  let text = value instanceof Error ? value.message : String(value);
70
88
  for (const secret of Object.values(config.environment).filter(Boolean).sort((a, b) => b.length - a.length))
@@ -97,7 +115,7 @@ var PI_CARD = {
97
115
  verifiedSkills: false,
98
116
  everydayHarness: false,
99
117
  capabilities: {
100
- sessionModelSwitch: "unsupported",
118
+ sessionModelSwitch: "in-session",
101
119
  runtimeModeSwitch: "next-turn",
102
120
  contextCompaction: "native",
103
121
  turnSteering: true
@@ -147,7 +165,8 @@ import {
147
165
  describeExecutorSpawnFailure,
148
166
  ExecutorSpawnAbortedError,
149
167
  ExecutorSpawnRefusedError,
150
- isExecutorSpawnFailure
168
+ isExecutorSpawnFailure,
169
+ raceAbort
151
170
  } from "@ycodium-ai/plugin-api/executor";
152
171
  import { runProcessOperation } from "@ycodium-ai/plugin-api/process-operation";
153
172
 
@@ -349,7 +368,7 @@ async function createLaunch(config, input, cursor) {
349
368
  baseUrl,
350
369
  api: apiProtocol,
351
370
  apiKey: `\${${PI_API_KEY_ENV}}`,
352
- models: [{ id: config.model }]
371
+ models: config.models.map((id) => ({ id }))
353
372
  }
354
373
  }
355
374
  }),
@@ -381,7 +400,7 @@ async function createLaunch(config, input, cursor) {
381
400
  "rpc",
382
401
  "--no-approve",
383
402
  "--model",
384
- `ycodium/${config.model}`,
403
+ `ycodium/${requestedModel(config, input?.modelSelection) ?? config.model}`,
385
404
  "--no-extensions",
386
405
  "--no-skills"
387
406
  ];
@@ -862,13 +881,6 @@ function createPiInstance(value, host) {
862
881
  const disposed = new AbortController();
863
882
  const now = () => (/* @__PURE__ */ new Date()).toISOString();
864
883
  const nativeMode = PI_NATIVE_MODE_REPORT;
865
- const validateModel = (selection) => {
866
- const model = string(record(selection).model).trim();
867
- if (model && model !== config.model && model !== `ycodium/${config.model}`)
868
- throw Object.assign(new Error("Pi can only run the model configured for this instance."), {
869
- code: "provider-rejected"
870
- });
871
- };
872
884
  const safeError = (cause) => {
873
885
  if (cause instanceof ExecutorSpawnRefusedError || cause instanceof ExecutorSpawnAbortedError)
874
886
  return cause;
@@ -992,6 +1004,8 @@ function createPiInstance(value, host) {
992
1004
  const abortTurn = async (booked, signal) => {
993
1005
  const turn = booked.turn, conn = booked.connection;
994
1006
  if (!turn || turn.completed || !conn) return;
1007
+ if (booked.dispatching) await raceAbort(booked.dispatching, signal);
1008
+ if (turn.completed) return;
995
1009
  await conn.rpc.request("clear_queue", {}, signal, 1e4);
996
1010
  await conn.rpc.request("abort", {}, signal, 1e4);
997
1011
  await waitForSettled(booked, turn, signal);
@@ -1019,7 +1033,7 @@ function createPiInstance(value, host) {
1019
1033
  const startOne = async (input, signal) => {
1020
1034
  ensureActive();
1021
1035
  validateConnection(config);
1022
- validateModel(input.modelSelection);
1036
+ const model = requestedModel(config, input.modelSelection) ?? config.model;
1023
1037
  signal?.throwIfAborted();
1024
1038
  const nativeModeId = piNativeModeFor(input.runtimeMode, input.nativeModeId);
1025
1039
  const existing = threads.get(input.threadId);
@@ -1037,7 +1051,7 @@ function createPiInstance(value, host) {
1037
1051
  runtimeMode: input.runtimeMode,
1038
1052
  nativeModeId,
1039
1053
  status: "starting",
1040
- model: config.model,
1054
+ model,
1041
1055
  createdAt: now(),
1042
1056
  updatedAt: now(),
1043
1057
  resumeCursor: cursor,
@@ -1199,10 +1213,13 @@ function createPiInstance(value, host) {
1199
1213
  startSession: start,
1200
1214
  sendTurn: async (input, signal) => {
1201
1215
  signal?.throwIfAborted();
1202
- validateModel(input.modelSelection);
1216
+ const requested = requestedModel(config, input.modelSelection);
1203
1217
  const booked = requireThread(input.threadId), conn = connection(booked);
1204
1218
  if (booked.stopping) throw new Error("Pi is stopping the current turn.");
1205
1219
  if (input.expectedTurnId !== void 0) {
1220
+ if (booked.dispatching) await raceAbort(booked.dispatching, signal);
1221
+ signal?.throwIfAborted();
1222
+ if (booked.stopping || booked.closing) throw new Error("Pi is stopping the current turn.");
1206
1223
  if (!booked.turn || booked.turn.id !== input.expectedTurnId || booked.turn.completed)
1207
1224
  throw Object.assign(new Error("The target Pi turn has ended."), {
1208
1225
  code: "provider-rejected"
@@ -1221,7 +1238,8 @@ function createPiInstance(value, host) {
1221
1238
  }
1222
1239
  if (booked.turn) throw new Error("Pi already has an active turn.");
1223
1240
  const cursor = record(booked.session.resumeCursor);
1224
- const turn = new PiTurn(booked.emit, config.model, string(cursor.sessionId), () => {
1241
+ const model = requested ?? booked.session.model ?? config.model;
1242
+ const turn = new PiTurn(booked.emit, model, string(cursor.sessionId), () => {
1225
1243
  if (booked.turn === turn) booked.turn = void 0;
1226
1244
  booked.session.status = "idle";
1227
1245
  booked.session.updatedAt = now();
@@ -1229,11 +1247,26 @@ function createPiInstance(value, host) {
1229
1247
  booked.turn = turn;
1230
1248
  booked.session.status = "running";
1231
1249
  turn.start();
1250
+ const issued = Promise.withResolvers();
1251
+ const dispatch = (async () => {
1252
+ try {
1253
+ if (model !== booked.session.model) {
1254
+ await conn.rpc.request("set_model", { provider: "ycodium", modelId: model }, signal);
1255
+ booked.session.model = model;
1256
+ }
1257
+ if (booked.stopping || booked.closing) return turn.finish("interrupted");
1258
+ const prompting = conn.rpc.request("prompt", { message: input.input ?? "" }, signal);
1259
+ issued.resolve();
1260
+ const response = record(await prompting);
1261
+ if (response.disposition === "handled") turn.finish("completed");
1262
+ } finally {
1263
+ issued.resolve();
1264
+ }
1265
+ })();
1266
+ const dispatched = issued.promise;
1267
+ booked.dispatching = dispatched;
1232
1268
  try {
1233
- const response = record(
1234
- await conn.rpc.request("prompt", { message: input.input ?? "" }, signal)
1235
- );
1236
- if (response.disposition === "handled") turn.finish("completed");
1269
+ await dispatch;
1237
1270
  return {
1238
1271
  threadId: input.threadId,
1239
1272
  turnId: turn.id,
@@ -1243,6 +1276,8 @@ function createPiInstance(value, host) {
1243
1276
  turn.finish("failed", redact(config, cause));
1244
1277
  if (!(cause instanceof PiResponseError)) await close(booked);
1245
1278
  throw safeError(cause);
1279
+ } finally {
1280
+ if (booked.dispatching === dispatched) booked.dispatching = void 0;
1246
1281
  }
1247
1282
  },
1248
1283
  interruptTurn: async (threadId, turnId, signal) => {
@@ -1375,7 +1410,7 @@ function createPiInstance(value, host) {
1375
1410
  // src/index.ts
1376
1411
  var index_default = definePlugin({
1377
1412
  id: "agent-pi",
1378
- apiVersion: 48,
1413
+ apiVersion: 49,
1379
1414
  executors: [
1380
1415
  { card: PI_CARD, createInstance: async (config, host) => createPiInstance(config, host) }
1381
1416
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-pi",
3
- "version": "0.2.3598",
3
+ "version": "0.2.3608",
4
4
  "description": "Pi executor over the native RPC protocol.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -30,9 +30,9 @@
30
30
  "tailwindcss": "^4.0.0",
31
31
  "typescript": "~6.0.3",
32
32
  "vite-plus": "0.2.2",
33
- "@ycodium-ai/plugin-api": "0.2.3598",
34
- "ycodium-executor-testing": "0.0.0",
35
- "@ycodium/contracts": "0.0.33"
33
+ "@ycodium-ai/plugin-api": "0.2.3608",
34
+ "@ycodium/contracts": "0.0.33",
35
+ "ycodium-executor-testing": "0.0.0"
36
36
  },
37
37
  "ycodium": {
38
38
  "plugin": "./dist/index.js",