@henryqw/pi-task-models 3.0.1 → 4.0.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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # `@henryqw/pi-task-models`
2
2
 
3
- Shared `fast`, `balanced`, `frontier`, and `fav` model profiles plus routing for consumer-owned Model Tasks.
3
+ Choose shared `fast`, `balanced`, `frontier`, and `fav` model profiles for consumer-owned Model Tasks.
4
4
 
5
5
  ## Why
6
6
 
7
- - **Created for**: Deduplicating model pickers and catalogs that each extension previously owned separately.
7
+ - **Created for**: Remove duplicated model pickers and catalogs that extensions once owned separately.
8
8
  - **Advantage**: One shared profile control plane keeps routes consistent while each consumer owns its task identity, intent, and default profile.
9
9
 
10
10
  ## Install
@@ -20,24 +20,35 @@ pi install npm:@henryqw/pi-task-models
20
20
  | `@henryqw/pi-auto-compact` | Consumer. Its local compaction task defaults to `fast`. |
21
21
  | `@henryqw/pi-herdr-btw` | Consumer. Its local side-thread task defaults to `fast`. |
22
22
  | `@henryqw/pi-herdr-rename` | Consumer. Its local rename task defaults to `fast`. |
23
- | `@henryqw/pi-subagent` | Consumer. Its local delegation task defaults to `fast`; callers can declare their own task. |
23
+ | `@henryqw/pi-memory` | Consumer. Its local candidate-review task defaults to `balanced`. |
24
24
  | `@henryqw/pi-multi-codex` | Improves. Numbered Codex slots dedupe to one route. |
25
+ | `@henryqw/pi-subagent` | Consumer. Its local delegation task defaults to `fast`; callers can declare their own task. |
25
26
 
26
27
  ## Use
27
28
 
28
- | Surface | Type | Purpose |
29
- | --- | --- | --- |
30
- | `/task-models` | command | Edit a profile or override an active task's default profile. |
29
+ Use `/task-models` to select a profile or override an active task's default profile.
30
+
31
+ ### Profile selection
32
+
33
+ Selecting a profile sets its primary model, primary thinking, optional fallback model, and fallback thinking. One completed flow writes the whole profile.
34
+
35
+ Selecting an active task sets its explicit override. Choosing its consumer-declared default removes that override.
31
36
 
32
- Selecting a profile sets primary model, primary thinking, optional fallback model, then fallback thinking. One completed flow writes the whole profile. Selecting an active task sets its explicit override; choosing its consumer-declared default removes that override.
37
+ ### Active declarations
33
38
 
34
- Consumers register their declarations at extension load. The one shared control plane asks active extensions for declarations when `/task-models` opens, so extension load order does not matter. It lists each active task's effective profile. Hidden explicit assignments stay stored when a consumer is disabled.
39
+ Consumers register declarations at extension load. When `/task-models` opens, the shared control plane asks active extensions for declarations. Extension load order does not matter.
35
40
 
36
- Menus and resolution use the current session's `ctx.scopedModels`, including pinned thinking. Empty scope uses Pi's full available model registry. Numbered Codex account aliases are deduplicated. Fallback choices exclude the selected primary. BTW selects the first authenticated viable route before pane launch.
41
+ The control plane lists each active task's effective profile. Hidden explicit assignments stay stored when a consumer is disabled.
42
+
43
+ ### Scoped models, aliases, and fallback
44
+
45
+ Menus and resolution use the current session's `ctx.scopedModels`, including pinned thinking. An empty scope uses Pi's full available model registry. Numbered Codex account aliases are deduplicated.
46
+
47
+ Fallback choices exclude the selected primary. BTW selects the first authenticated viable route before pane launch.
37
48
 
38
49
  ## Config
39
50
 
40
- Single shared JSON file 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.
51
+ 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.
41
52
 
42
53
  ```json
43
54
  {
@@ -71,8 +82,33 @@ Single shared JSON file at the exact package-owned path `~/.pi/agent/config/pi-t
71
82
  | `tasks` | No | Object mapping task IDs (`<package>/<task>`) to explicit user profile overrides | `{}` |
72
83
  | `tasks.<taskId>` | Value required if the key is present | `fast`, `balanced`, `frontier`, `fav` | That task declaration's `defaultProfile` |
73
84
 
74
- Task defaults live only in consumer declarations. Existing explicit assignments, including one equal to a declaration's default, remain valid. 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>`.
85
+ Pi's model registry, including session-scoped models, is the source of available models at resolution. This file does not contain a model catalog.
86
+
87
+ Task defaults live only in consumer declarations. Existing explicit assignments, including one equal to a declaration's default, remain valid.
88
+
89
+ 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>`.
75
90
 
76
- Reads are strict. A missing file yields `{ "profiles": {}, "tasks": {} }`; malformed JSON, unknown keys, invalid task IDs, unknown profiles, or invalid profile or route values fail visibly with `/task-models` guidance and never rewrite the file.
91
+ Reads are strict. `loadTaskModelsConfig()` returns `{ source: "missing", value: { "profiles": {}, "tasks": {} } }` for a missing file. It does not create a file.
92
+
93
+ At session start, task-models warns when `~/.pi/agent/config/pi-task-models/config.json` is missing; run `/task-models` to configure task routes.
94
+
95
+ 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.
96
+
97
+ ## For extension authors
98
+
99
+ A `ModelTask` is a consumer-owned independently executed model operation. Consumers define a `ModelTask` and call `registerModelTask(pi, task)` at extension load.
100
+
101
+ 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.
102
+
103
+ 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`.
104
+
105
+ Resolution errors are `TaskRouteError` values. Check `taskRouteCode`:
106
+
107
+ | Code | Meaning |
108
+ | --- | --- |
109
+ | `config-missing` | The optional shared config file is absent. |
110
+ | `config-read` | A present shared config file cannot be read or validated. |
111
+ | `profile-missing` | The selected profile is not configured. |
112
+ | `no-route` | The selected profile has no available route. |
77
113
 
78
- Profile thinking is authoritative for task routes. Consumers define a `ModelTask`, call `registerModelTask(pi, task)` at extension load, then call `resolveConfiguredTaskRoute(ctx, task)` or `resolveConfiguredTaskRoutes(ctx, task)`. Resolution uses `config.tasks[task.id] ?? task.defaultProfile`.
114
+ 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.1",
3
+ "version": "4.0.0",
4
4
  "description": "Shared task model profiles and routing for HenryQW Pi extensions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -14,11 +14,11 @@
14
14
  },
15
15
  "license": "MIT",
16
16
  "files": [
17
+ "LICENSE",
17
18
  "dist",
18
19
  "extensions",
19
20
  "README.md",
20
- "CONTEXT.md",
21
- "LICENSE"
21
+ "CONTEXT.md"
22
22
  ],
23
23
  "types": "./dist/index.d.ts",
24
24
  "exports": {
@@ -53,5 +53,8 @@
53
53
  "extensions": [
54
54
  "./extensions/task-models.ts"
55
55
  ]
56
+ },
57
+ "dependencies": {
58
+ "@henryqw/pi-config-store": "^0.1.0"
56
59
  }
57
60
  }