@henryqw/pi-task-models 3.0.2 → 4.0.1

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Choose shared `fast`, `balanced`, `frontier`, and `fav` model profiles for consumer-owned Model Tasks.
4
4
 
5
+ ![Pi showing task model profiles and task routes](./example.png)
6
+
5
7
  ## Why
6
8
 
7
9
  - **Created for**: Remove duplicated model pickers and catalogs that extensions once owned separately.
@@ -48,7 +50,7 @@ Fallback choices exclude the selected primary. BTW selects the first authenticat
48
50
 
49
51
  ## Config
50
52
 
51
- The single shared JSON file is at the exact package-owned path `~/.pi/agent/config/pi-task-models.json`. Consumers read it but never write it. Only explicit `/task-models` actions save it.
53
+ The shared JSON file is at `~/.pi/agent/config/pi-task-models/config.json`. Consumers use `loadTaskModelsConfig()` for validated values. They never read or write this file. Only explicit `/task-models` actions save it.
52
54
 
53
55
  ```json
54
56
  {
@@ -88,12 +90,27 @@ Task defaults live only in consumer declarations. Existing explicit assignments,
88
90
 
89
91
  Model references use canonical `provider/model`. Numbered Codex account aliases (`openai-codex-N`) resolve through Pi's registry and store canonically as `openai-codex/<model>`.
90
92
 
91
- Reads are strict. A missing file yields `{ "profiles": {}, "tasks": {} }`.
93
+ Reads are strict. `loadTaskModelsConfig()` returns `{ source: "missing", value: { "profiles": {}, "tasks": {} } }` for a missing file. It does not create a file.
94
+
95
+ At session start, task-models warns when `~/.pi/agent/config/pi-task-models/config.json` is missing; run `/task-models` to configure task routes.
92
96
 
93
- Malformed JSON, unknown keys, invalid task IDs, unknown profiles, or invalid profile or route values fail visibly with `/task-models` guidance. The file is never rewritten.
97
+ Malformed JSON, unknown keys, invalid task IDs, unknown profiles, or invalid profile or route values fail visibly with `/task-models` guidance. The malformed file is preserved.
94
98
 
95
99
  ## For extension authors
96
100
 
97
101
  A `ModelTask` is a consumer-owned independently executed model operation. Consumers define a `ModelTask` and call `registerModelTask(pi, task)` at extension load.
98
102
 
103
+ Use `loadTaskModelsConfig()` to get validated config without reading a file. Its `source` is `"file"` or `"missing"`, so consumers can warn when defaults are in use.
104
+
99
105
  Use `resolveConfiguredTaskRoute(ctx, task)` or `resolveConfiguredTaskRoutes(ctx, task)` to resolve routes. Profile thinking is authoritative for task routes. Resolution uses `config.tasks[task.id] ?? task.defaultProfile`.
106
+
107
+ Resolution errors are `TaskRouteError` values. Check `taskRouteCode`:
108
+
109
+ | Code | Meaning |
110
+ | --- | --- |
111
+ | `config-missing` | The optional shared config file is absent. |
112
+ | `config-read` | A present shared config file cannot be read or validated. |
113
+ | `profile-missing` | The selected profile is not configured. |
114
+ | `no-route` | The selected profile has no available route. |
115
+
116
+ Every error directs users to `/task-models`. A consumer may silence only `config-missing` when it has a safe current-session fallback.
package/dist/index.d.ts CHANGED
@@ -28,9 +28,10 @@ export type ResolvedTaskRoute = {
28
28
  model: AvailableModel;
29
29
  thinkingLevel: ThinkingLevel;
30
30
  };
31
- export declare const configPath: (agentDir?: string) => string;
32
- export declare function readTaskModelsConfig(agentDir?: string): TaskModelsConfig;
33
- export declare function writeTaskModelsConfig(config: TaskModelsConfig, agentDir?: string): void;
31
+ export declare function loadTaskModelsConfig(agentDir?: string): {
32
+ source: "file" | "missing";
33
+ value: TaskModelsConfig;
34
+ };
34
35
  export declare function canonicalModelReference(model: {
35
36
  provider: string;
36
37
  id: string;
@@ -45,7 +46,7 @@ export declare function availableTaskModels(ctx: ExtensionContext): AvailableMod
45
46
  export declare function taskThinkingLevels(ctx: ExtensionContext, model: AvailableModel): ThinkingLevel[];
46
47
  export declare function resolveTaskModelRoute(ctx: ExtensionContext, route: TaskModelRoute, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute | undefined;
47
48
  export declare function resolveConfiguredTaskRoutes(ctx: ExtensionContext, task: ModelTask, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute[];
48
- export type TaskRouteErrorCode = "config-read" | "profile-missing" | "no-route";
49
+ export type TaskRouteErrorCode = "config-missing" | "config-read" | "profile-missing" | "no-route";
49
50
  export type TaskRouteError = Error & {
50
51
  taskRouteCode: TaskRouteErrorCode;
51
52
  profileName?: ProfileName;
package/dist/index.js CHANGED
@@ -1,16 +1,13 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
1
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
4
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
+ import { createConfigStore } from "@henryqw/pi-config-store";
5
4
  export const PROFILE_NAMES = ["fast", "balanced", "frontier", "fav"];
6
5
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
7
6
  const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
8
- const CONFIG_FILE = "pi-task-models.json";
9
7
  const MODEL_TASK_REQUEST_EVENT = "@henryqw/pi-task-models:model-task-request";
10
8
  const MODEL_TASK_RESPONSE_EVENT = "@henryqw/pi-task-models:model-task-response";
11
9
  const registeredModelTasks = new WeakMap();
12
10
  let modelTaskRequestNumber = 0;
13
- export const configPath = (agentDir = getAgentDir()) => join(agentDir, "config", CONFIG_FILE);
14
11
  function isCodexProvider(provider) {
15
12
  return provider === "openai-codex" || Boolean(provider && CODEX_ALIAS.test(provider));
16
13
  }
@@ -89,15 +86,6 @@ function isTaskProfile(value) {
89
86
  function normalizeRoute(route) {
90
87
  return { model: canonicalModelReference(route.model), thinkingLevel: route.thinkingLevel };
91
88
  }
92
- function normalizeConfig(config) {
93
- return {
94
- profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, {
95
- primary: normalizeRoute(profile.primary),
96
- ...(profile.fallback ? { fallback: normalizeRoute(profile.fallback) } : {}),
97
- }])),
98
- tasks: { ...config.tasks },
99
- };
100
- }
101
89
  function parseConfig(value) {
102
90
  if (!value || typeof value !== "object" || Array.isArray(value))
103
91
  throw new Error("Config must be an object.");
@@ -136,24 +124,16 @@ function parseConfig(value) {
136
124
  }
137
125
  return { profiles, tasks };
138
126
  }
139
- export function readTaskModelsConfig(agentDir = getAgentDir()) {
140
- try {
141
- const value = JSON.parse(readFileSync(configPath(agentDir), "utf8"));
142
- return parseConfig(value);
143
- }
144
- catch (error) {
145
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
146
- return { profiles: {}, tasks: {} };
147
- }
148
- throw error;
149
- }
127
+ function taskModelsConfigStore(agentDir = getAgentDir()) {
128
+ return createConfigStore({
129
+ extensionId: "pi-task-models",
130
+ agentDir,
131
+ defaults: () => ({ profiles: {}, tasks: {} }),
132
+ parse: parseConfig,
133
+ });
150
134
  }
151
- export function writeTaskModelsConfig(config, agentDir = getAgentDir()) {
152
- if (config.profiles.fav?.fallback)
153
- throw new Error("fav profile has no fallback.");
154
- const file = configPath(agentDir);
155
- mkdirSync(dirname(file), { recursive: true });
156
- writeFileSync(file, `${JSON.stringify(normalizeConfig(config), null, 2)}\n`);
135
+ export function loadTaskModelsConfig(agentDir = getAgentDir()) {
136
+ return taskModelsConfigStore(agentDir).loadSync();
157
137
  }
158
138
  export function canonicalModelReference(model) {
159
139
  const reference = typeof model === "string" ? model : `${model.provider}/${model.id}`;
@@ -237,13 +217,17 @@ export function resolveTaskModelRoute(ctx, route, agentDir = getAgentDir(), thin
237
217
  }
238
218
  export function resolveConfiguredTaskRoutes(ctx, task, agentDir = getAgentDir(), thinking) {
239
219
  const declaration = validatedModelTask(task);
240
- let config;
220
+ let loadedConfig;
241
221
  try {
242
- config = readTaskModelsConfig(agentDir);
222
+ loadedConfig = loadTaskModelsConfig(agentDir);
243
223
  }
244
224
  catch {
245
225
  throw taskRouteError("config-read", "Couldn't read task model config. Run /task-models.");
246
226
  }
227
+ if (loadedConfig.source === "missing") {
228
+ throw taskRouteError("config-missing", "Task model config is missing. Run /task-models to configure task routes.");
229
+ }
230
+ const config = loadedConfig.value;
247
231
  const profileName = config.tasks[declaration.id] ?? declaration.defaultProfile;
248
232
  const profile = config.profiles[profileName];
249
233
  if (!profile)
@@ -351,12 +335,18 @@ function discoverModelTasks(pi) {
351
335
  }
352
336
  export function createTaskModelsExtension(pi, options) {
353
337
  const agentDir = options?.agentDir ?? getAgentDir();
338
+ const configStore = taskModelsConfigStore(agentDir);
339
+ pi.on("session_start", (_event, ctx) => {
340
+ if (configStore.loadSync().source === "missing") {
341
+ ctx.ui.notify(`Task model config is missing at ${configStore.path}; run /task-models to configure task routes.`, "warning");
342
+ }
343
+ });
354
344
  pi.registerCommand("task-models", {
355
345
  description: "configure shared task model profiles",
356
346
  handler: async (_args, ctx) => {
357
347
  let config;
358
348
  try {
359
- config = readTaskModelsConfig(agentDir);
349
+ config = configStore.loadSync().value;
360
350
  }
361
351
  catch {
362
352
  ctx.ui.notify("Couldn't read task model config.", "error");
@@ -371,9 +361,9 @@ export function createTaskModelsExtension(pi, options) {
371
361
  : `${name} · not configured`,
372
362
  };
373
363
  });
374
- const save = () => {
364
+ const save = async () => {
375
365
  try {
376
- writeTaskModelsConfig(config, agentDir);
366
+ await configStore.save(config);
377
367
  return true;
378
368
  }
379
369
  catch {
@@ -400,7 +390,7 @@ export function createTaskModelsExtension(pi, options) {
400
390
  delete config.tasks[task.id];
401
391
  else
402
392
  config.tasks[task.id] = profile;
403
- if (!save())
393
+ if (!await save())
404
394
  return;
405
395
  ctx.ui.notify(`${task.id} assigned to ${profile}.`, "info");
406
396
  return;
@@ -434,7 +424,7 @@ export function createTaskModelsExtension(pi, options) {
434
424
  return;
435
425
  config.profiles[profile] = { primary, ...(fallback ? { fallback } : {}) };
436
426
  }
437
- if (!save())
427
+ if (!await save())
438
428
  return;
439
429
  ctx.ui.notify(`${profile} profile saved.`, "info");
440
430
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-task-models",
3
- "version": "3.0.2",
3
+ "version": "4.0.1",
4
4
  "description": "Shared task model profiles and routing for HenryQW Pi extensions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "git+https://github.com/HenryQW/pi-packages.git",
44
- "directory": "packages/pi-task-models"
43
+ "url": "git+https://github.com/HenryQW/pi-harness.git",
44
+ "directory": "extensions/pi-task-models"
45
45
  },
46
46
  "bugs": {
47
- "url": "https://github.com/HenryQW/pi-packages/issues"
47
+ "url": "https://github.com/HenryQW/pi-harness/issues"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
@@ -52,6 +52,10 @@
52
52
  "pi": {
53
53
  "extensions": [
54
54
  "./extensions/task-models.ts"
55
- ]
55
+ ],
56
+ "image": "https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-task-models/example.png"
57
+ },
58
+ "dependencies": {
59
+ "@henryqw/pi-config-store": "^0.1.0"
56
60
  }
57
61
  }