@henryqw/pi-task-models 2.0.0 → 3.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/CONTEXT.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Pi Task Models
2
2
 
3
- Pi Task Models stores shared task profiles and task-to-profile assignments for HenryQW extensions; `pi-subagent/delegateTask` defaults to `fast`.
3
+ Pi Task Models stores shared Task Profiles and explicit user overrides for consumer-owned Model Tasks. Consumers declare identity, intent, and default profiles; this package has no consumer catalog or package/source discovery.
4
4
 
5
5
  ## Language
6
6
 
7
+ **Model Task**:
8
+ A consumer-owned independently executed model operation. Exists only for independent model execution — not for work performed by the current session agent. Each consumer owns its Model Task ID, label, purpose, execution/prompt, and default Task Profile.
9
+ _Avoid_: package-wide model setting, centrally owned consumer-task catalog, current-agent work
10
+
7
11
  **Task Profile**:
8
12
  Named shared route set (`fast`, `balanced`, `frontier`, or `fav`) with a required primary route and optional fallback route (`fav` has no fallback).
9
13
  _Avoid_: package-owned model picker, per-extension model catalog
@@ -12,6 +16,6 @@ _Avoid_: package-owned model picker, per-extension model catalog
12
16
  One model reference plus thinking level chosen from current Pi model scope. Empty scope means every available registry model; pinned scope thinking remains binding.
13
17
  _Avoid_: free-form provider path, copied model metadata
14
18
 
15
- **Active Task Package**:
16
- Installed HenryQW package discovered from Pi command/tool sourceInfo.
17
- _Avoid_: filesystem scan, settings.json scan
19
+ **Task declaration discovery**:
20
+ The single control plane requests active consumer declarations through an idempotent namespaced Pi event request/response handshake when `/task-models` opens. It shows each declaration's effective `config.tasks[id] ?? defaultProfile`; inactive explicit overrides remain stored.
21
+ _Avoid_: filesystem scan, command/tool `sourceInfo` discovery, bundled consumer control planes
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # `@henryqw/pi-task-models`
2
2
 
3
- Shared `fast`, `balanced`, `frontier`, and `fav` model profiles for HenryQW Pi extensions.
3
+ Shared `fast`, `balanced`, `frontier`, and `fav` model profiles plus routing for consumer-owned Model Tasks.
4
4
 
5
5
  ## Why
6
6
 
7
7
  - **Created for**: Deduplicating model pickers and catalogs that each extension previously owned separately.
8
- - **Advantage**: One shared source of `fast`/`balanced`/`frontier`/`fav` profiles keeps routing consistent across consumers.
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
11
11
 
@@ -17,23 +17,23 @@ pi install npm:@henryqw/pi-task-models
17
17
 
18
18
  | Package | Why |
19
19
  | --- | --- |
20
- | `@henryqw/pi-auto-compact` | Consumer. Compaction defaults to the `fast` profile. |
21
- | `@henryqw/pi-herdr-btw` | Consumer. Side-thread launch uses the `fast` profile. |
22
- | `@henryqw/pi-herdr-rename` | Consumer. Rename uses the `fast` profile. |
23
- | `@henryqw/pi-subagent` | Consumer. Delegation defaults to `fast`; caller may override it. |
20
+ | `@henryqw/pi-auto-compact` | Consumer. Its local compaction task defaults to `fast`. |
21
+ | `@henryqw/pi-herdr-btw` | Consumer. Its local side-thread task defaults to `fast`. |
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. |
24
24
  | `@henryqw/pi-multi-codex` | Improves. Numbered Codex slots dedupe to one route. |
25
25
 
26
26
  ## Use
27
27
 
28
28
  | Surface | Type | Purpose |
29
29
  | --- | --- | --- |
30
- | `/task-models` | command | Edit a profile or assign a task to a profile. |
30
+ | `/task-models` | command | Edit a profile or override an active task's default profile. |
31
31
 
32
- Selecting a profile sets primary model, primary thinking, optional fallback model, then fallback thinking. One completed flow writes the whole profile. Selecting a task changes its assignment.
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.
33
33
 
34
- 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. Hidden task assignments stay stored when a package is disabled. BTW selects the first authenticated viable route before pane launch.
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.
35
35
 
36
- Package also exports config and route-resolution helpers for consumers.
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.
37
37
 
38
38
  ## Config
39
39
 
@@ -57,10 +57,7 @@ Single shared JSON file at the exact package-owned path `~/.pi/agent/config/pi-t
57
57
  }
58
58
  },
59
59
  "tasks": {
60
- "pi-herdr-btw/btw": "fast",
61
- "pi-herdr-rename/rename": "fast",
62
- "pi-auto-compact/autoCompact": "fast",
63
- "pi-subagent/delegateTask": "fast"
60
+ "pi-herdr-btw/btw": "balanced"
64
61
  }
65
62
  }
66
63
  ```
@@ -71,13 +68,11 @@ Single shared JSON file at the exact package-owned path `~/.pi/agent/config/pi-t
71
68
  | `profiles.<profile>.primary.model` | Yes within a configured profile's `primary` | Canonical `provider/model` reference without whitespace or NUL; available models come from Pi's model registry (or session-scoped models) at resolution time, not from this file | — |
72
69
  | `profiles.<profile>.primary.thinkingLevel` | Yes within a configured profile's `primary` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; the model must support the level when the route resolves | — |
73
70
  | `profiles.<profile>.fallback` | No | When present, requires both `model` and `thinkingLevel` with the corresponding `primary.*` values; not allowed for `fav` | Omitted |
74
- | `tasks` | No | Object mapping task IDs (`<package>/<task>`) to a profile name | Built-in defaults listed in the example above |
75
- | `tasks.<taskId>` | Value required if the key is present | `fast`, `balanced`, `frontier`, `fav` | The built-in default for that task, if any |
71
+ | `tasks` | No | Object mapping task IDs (`<package>/<task>`) to explicit user profile overrides | `{}` |
72
+ | `tasks.<taskId>` | Value required if the key is present | `fast`, `balanced`, `frontier`, `fav` | That task declaration's `defaultProfile` |
76
73
 
77
- 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>`.
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>`.
78
75
 
79
- Reads are strict. A missing file yields no profiles and the built-in default task assignments; 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.
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.
80
77
 
81
- Profile thinking is authoritative for task routes.
82
-
83
- Consumers can call `resolveConfiguredTaskRoute(ctx, taskId)` for the first usable route or `resolveConfiguredTaskRoutes(ctx, taskId)` for primary and fallback routes. Both read strict shared config and fail with `/task-models` guidance when assignment, profile, or route is unavailable.
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`.
package/dist/index.d.ts CHANGED
@@ -3,12 +3,13 @@ export declare const PROFILE_NAMES: readonly ["fast", "balanced", "frontier", "f
3
3
  export type ProfileName = (typeof PROFILE_NAMES)[number];
4
4
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
5
5
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
6
- export declare const DEFAULT_TASK_ASSIGNMENTS: {
7
- readonly "pi-herdr-btw/btw": "fast";
8
- readonly "pi-herdr-rename/rename": "fast";
9
- readonly "pi-auto-compact/autoCompact": "fast";
10
- readonly "pi-subagent/delegateTask": "fast";
11
- };
6
+ /** A consumer-owned independently executed model operation. */
7
+ export type ModelTask = Readonly<{
8
+ id: string;
9
+ label: string;
10
+ purpose: string;
11
+ defaultProfile: ProfileName;
12
+ }>;
12
13
  export type TaskModelRoute = {
13
14
  model: string;
14
15
  thinkingLevel: ThinkingLevel;
@@ -19,6 +20,7 @@ export type TaskModelProfile = {
19
20
  };
20
21
  export type TaskModelsConfig = {
21
22
  profiles: Partial<Record<ProfileName, TaskModelProfile>>;
23
+ /** Explicit user overrides only; consumer defaults are declared at runtime. */
22
24
  tasks: Record<string, ProfileName>;
23
25
  };
24
26
  export type AvailableModel = ReturnType<ExtensionContext["modelRegistry"]["getAvailable"]>[number];
@@ -26,10 +28,6 @@ export type ResolvedTaskRoute = {
26
28
  model: AvailableModel;
27
29
  thinkingLevel: ThinkingLevel;
28
30
  };
29
- export type ActiveTaskPackage = {
30
- packageName: string;
31
- task: string;
32
- };
33
31
  export declare const configPath: (agentDir?: string) => string;
34
32
  export declare function readTaskModelsConfig(agentDir?: string): TaskModelsConfig;
35
33
  export declare function writeTaskModelsConfig(config: TaskModelsConfig, agentDir?: string): void;
@@ -46,15 +44,16 @@ export declare function resolveAvailableModel(models: readonly AvailableModel[],
46
44
  export declare function availableTaskModels(ctx: ExtensionContext): AvailableModel[];
47
45
  export declare function taskThinkingLevels(ctx: ExtensionContext, model: AvailableModel): ThinkingLevel[];
48
46
  export declare function resolveTaskModelRoute(ctx: ExtensionContext, route: TaskModelRoute, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute | undefined;
49
- export declare function resolveConfiguredTaskRoutes(ctx: ExtensionContext, task: string, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute[];
50
- export type TaskRouteErrorCode = "config-read" | "task-unassigned" | "profile-missing" | "no-route";
47
+ export declare function resolveConfiguredTaskRoutes(ctx: ExtensionContext, task: ModelTask, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute[];
48
+ export type TaskRouteErrorCode = "config-read" | "profile-missing" | "no-route";
51
49
  export type TaskRouteError = Error & {
52
50
  taskRouteCode: TaskRouteErrorCode;
53
51
  profileName?: ProfileName;
54
52
  };
55
- export declare function resolveConfiguredTaskRoute(ctx: ExtensionContext, task: string, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute;
53
+ export declare function resolveConfiguredTaskRoute(ctx: ExtensionContext, task: ModelTask, agentDir?: string, thinking?: ThinkingLevel): ResolvedTaskRoute;
56
54
  export declare function orderedProfileRoutes(profile: TaskModelProfile): TaskModelRoute[];
57
- export declare function activeTaskPackages(pi: Pick<ExtensionAPI, "getCommands" | "getAllTools">, tasks?: Readonly<Record<string, ProfileName>>): ActiveTaskPackage[];
55
+ /** Register one consumer-owned declaration for on-demand control-plane discovery. */
56
+ export declare function registerModelTask(pi: Pick<ExtensionAPI, "events">, declaration: ModelTask): void;
58
57
  export declare function createTaskModelsExtension(pi: ExtensionAPI, options?: {
59
58
  agentDir?: string;
60
59
  }): void;
package/dist/index.js CHANGED
@@ -4,14 +4,12 @@ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  export const PROFILE_NAMES = ["fast", "balanced", "frontier", "fav"];
6
6
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
7
- export const DEFAULT_TASK_ASSIGNMENTS = {
8
- "pi-herdr-btw/btw": "fast",
9
- "pi-herdr-rename/rename": "fast",
10
- "pi-auto-compact/autoCompact": "fast",
11
- "pi-subagent/delegateTask": "fast",
12
- };
13
7
  const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
14
8
  const CONFIG_FILE = "pi-task-models.json";
9
+ const MODEL_TASK_REQUEST_EVENT = "@henryqw/pi-task-models:model-task-request";
10
+ const MODEL_TASK_RESPONSE_EVENT = "@henryqw/pi-task-models:model-task-response";
11
+ const registeredModelTasks = new WeakMap();
12
+ let modelTaskRequestNumber = 0;
15
13
  export const configPath = (agentDir = getAgentDir()) => join(agentDir, "config", CONFIG_FILE);
16
14
  function isCodexProvider(provider) {
17
15
  return provider === "openai-codex" || Boolean(provider && CODEX_ALIAS.test(provider));
@@ -25,6 +23,9 @@ function isThinkingLevel(value) {
25
23
  function hasOnlyKeys(value, keys) {
26
24
  return Object.keys(value).every((key) => keys.includes(key));
27
25
  }
26
+ function hasRequiredKeys(value, keys) {
27
+ return hasOnlyKeys(value, keys) && keys.every((key) => Object.hasOwn(value, key));
28
+ }
28
29
  function isModelReference(value) {
29
30
  return typeof value === "string"
30
31
  && value === value.trim()
@@ -34,6 +35,41 @@ function isModelReference(value) {
34
35
  function isTaskId(value) {
35
36
  return /^[a-z0-9][a-z0-9-]*\/[A-Za-z0-9][A-Za-z0-9-]*$/.test(value);
36
37
  }
38
+ function isNonEmptyText(value) {
39
+ return typeof value === "string"
40
+ && value === value.trim()
41
+ && value.length > 0
42
+ && !/[\u0000-\u001f\u007f-\u009f]/.test(value);
43
+ }
44
+ function parseModelTask(value) {
45
+ try {
46
+ if (!value || typeof value !== "object" || Array.isArray(value))
47
+ return;
48
+ const task = value;
49
+ if (!hasRequiredKeys(task, ["id", "label", "purpose", "defaultProfile"])
50
+ || !isNonEmptyText(task.id)
51
+ || !isTaskId(task.id)
52
+ || !isNonEmptyText(task.label)
53
+ || !isNonEmptyText(task.purpose)
54
+ || !isProfileName(task.defaultProfile))
55
+ return;
56
+ return {
57
+ id: task.id,
58
+ label: task.label,
59
+ purpose: task.purpose,
60
+ defaultProfile: task.defaultProfile,
61
+ };
62
+ }
63
+ catch {
64
+ return;
65
+ }
66
+ }
67
+ function validatedModelTask(value) {
68
+ const task = parseModelTask(value);
69
+ if (!task)
70
+ throw new Error("Model Task declaration is invalid.");
71
+ return task;
72
+ }
37
73
  function isTaskRoute(value) {
38
74
  if (!value || typeof value !== "object" || Array.isArray(value))
39
75
  return false;
@@ -69,7 +105,7 @@ function parseConfig(value) {
69
105
  if (!hasOnlyKeys(record, ["profiles", "tasks"]))
70
106
  throw new Error("Config contains unknown settings.");
71
107
  const profiles = {};
72
- const tasks = { ...DEFAULT_TASK_ASSIGNMENTS };
108
+ const tasks = {};
73
109
  if (record.profiles !== undefined) {
74
110
  if (!record.profiles || typeof record.profiles !== "object" || Array.isArray(record.profiles)) {
75
111
  throw new Error("profiles must be an object.");
@@ -107,7 +143,7 @@ export function readTaskModelsConfig(agentDir = getAgentDir()) {
107
143
  }
108
144
  catch (error) {
109
145
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
110
- return { profiles: {}, tasks: { ...DEFAULT_TASK_ASSIGNMENTS } };
146
+ return { profiles: {}, tasks: {} };
111
147
  }
112
148
  throw error;
113
149
  }
@@ -200,6 +236,7 @@ export function resolveTaskModelRoute(ctx, route, agentDir = getAgentDir(), thin
200
236
  return { model, thinkingLevel: route.thinkingLevel };
201
237
  }
202
238
  export function resolveConfiguredTaskRoutes(ctx, task, agentDir = getAgentDir(), thinking) {
239
+ const declaration = validatedModelTask(task);
203
240
  let config;
204
241
  try {
205
242
  config = readTaskModelsConfig(agentDir);
@@ -207,12 +244,10 @@ export function resolveConfiguredTaskRoutes(ctx, task, agentDir = getAgentDir(),
207
244
  catch {
208
245
  throw taskRouteError("config-read", "Couldn't read task model config. Run /task-models.");
209
246
  }
210
- const profileName = config.tasks[task];
211
- if (!profileName)
212
- throw taskRouteError("task-unassigned", `Task ${task} is not assigned to a profile. Run /task-models.`);
247
+ const profileName = config.tasks[declaration.id] ?? declaration.defaultProfile;
213
248
  const profile = config.profiles[profileName];
214
249
  if (!profile)
215
- throw taskRouteError("profile-missing", `Task ${task} profile ${profileName} is not configured. Run /task-models.`, profileName);
250
+ throw taskRouteError("profile-missing", `Task ${declaration.id} profile ${profileName} is not configured. Run /task-models.`, profileName);
216
251
  const routes = [];
217
252
  for (const route of orderedProfileRoutes(profile)) {
218
253
  const resolved = resolveTaskModelRoute(ctx, route, agentDir, thinking);
@@ -220,7 +255,7 @@ export function resolveConfiguredTaskRoutes(ctx, task, agentDir = getAgentDir(),
220
255
  routes.push(resolved);
221
256
  }
222
257
  if (!routes.length) {
223
- throw taskRouteError("no-route", `Task ${task} profile ${profileName} has no available route${thinking ? ` supporting thinking ${thinking}` : ""}. Run /task-models.`, profileName);
258
+ throw taskRouteError("no-route", `Task ${declaration.id} profile ${profileName} has no available route${thinking ? ` supporting thinking ${thinking}` : ""}. Run /task-models.`, profileName);
224
259
  }
225
260
  return routes;
226
261
  }
@@ -233,28 +268,86 @@ export function resolveConfiguredTaskRoute(ctx, task, agentDir = getAgentDir(),
233
268
  export function orderedProfileRoutes(profile) {
234
269
  return profile.fallback ? [profile.primary, profile.fallback] : [profile.primary];
235
270
  }
236
- export function activeTaskPackages(pi, tasks = DEFAULT_TASK_ASSIGNMENTS) {
237
- const sources = [
238
- ...pi.getCommands().map((command) => command.sourceInfo),
239
- ...pi.getAllTools().map((tool) => tool.sourceInfo),
240
- ];
241
- return Object.keys(tasks).flatMap((task) => {
242
- if (!isTaskId(task))
243
- return [];
244
- const packageName = `@henryqw/${task.slice(0, task.indexOf("/"))}`;
245
- return sources.some((source) => sourceMatchesPackage(source, packageName))
246
- ? [{ packageName, task }]
247
- : [];
271
+ function sameModelTask(left, right) {
272
+ return left.id === right.id
273
+ && left.label === right.label
274
+ && left.purpose === right.purpose
275
+ && left.defaultProfile === right.defaultProfile;
276
+ }
277
+ function parseModelTaskDiscoveryRequest(value) {
278
+ try {
279
+ if (!value || typeof value !== "object" || Array.isArray(value))
280
+ return;
281
+ const request = value;
282
+ return hasRequiredKeys(request, ["requestId"]) && isNonEmptyText(request.requestId)
283
+ ? { requestId: request.requestId }
284
+ : undefined;
285
+ }
286
+ catch {
287
+ return;
288
+ }
289
+ }
290
+ function parseModelTaskDiscoveryResponse(value) {
291
+ try {
292
+ if (!value || typeof value !== "object" || Array.isArray(value))
293
+ return;
294
+ const response = value;
295
+ if (!hasRequiredKeys(response, ["requestId", "task"]) || !isNonEmptyText(response.requestId))
296
+ return;
297
+ const task = parseModelTask(response.task);
298
+ return task ? { requestId: response.requestId, task } : undefined;
299
+ }
300
+ catch {
301
+ return;
302
+ }
303
+ }
304
+ /** Register one consumer-owned declaration for on-demand control-plane discovery. */
305
+ export function registerModelTask(pi, declaration) {
306
+ const task = validatedModelTask(declaration);
307
+ let tasks = registeredModelTasks.get(pi);
308
+ if (!tasks) {
309
+ tasks = new Map();
310
+ registeredModelTasks.set(pi, tasks);
311
+ }
312
+ const existing = tasks.get(task.id);
313
+ if (existing) {
314
+ if (!sameModelTask(existing, task))
315
+ throw new Error(`Conflicting Model Task declaration: ${task.id}.`);
316
+ return;
317
+ }
318
+ tasks.set(task.id, task);
319
+ pi.events.on(MODEL_TASK_REQUEST_EVENT, (payload) => {
320
+ // Event payloads cross extension boundaries and are not trusted.
321
+ const request = parseModelTaskDiscoveryRequest(payload);
322
+ if (!request)
323
+ return;
324
+ pi.events.emit(MODEL_TASK_RESPONSE_EVENT, { requestId: request.requestId, task });
248
325
  });
249
326
  }
250
- function sourceMatchesPackage(sourceInfo, packageName) {
251
- const npmSource = `npm:${packageName}`;
252
- if (sourceInfo.source === packageName || sourceInfo.source === npmSource || sourceInfo.source.startsWith(`${npmSource}@`))
253
- return true;
254
- const path = sourceInfo.path.replaceAll("\\", "/");
255
- const shortName = packageName.split("/").pop();
256
- return path.includes(`/node_modules/${packageName}/`)
257
- || Boolean(shortName && path.includes(`/packages/${shortName}/`));
327
+ function discoverModelTasks(pi) {
328
+ const requestId = `model-task-${++modelTaskRequestNumber}`;
329
+ const tasks = new Map();
330
+ const conflicts = new Set();
331
+ const off = pi.events.on(MODEL_TASK_RESPONSE_EVENT, (payload) => {
332
+ // Event payloads cross extension boundaries and are not trusted.
333
+ const response = parseModelTaskDiscoveryResponse(payload);
334
+ if (!response || response.requestId !== requestId)
335
+ return;
336
+ const existing = tasks.get(response.task.id);
337
+ if (existing && !sameModelTask(existing, response.task))
338
+ conflicts.add(response.task.id);
339
+ else
340
+ tasks.set(response.task.id, response.task);
341
+ });
342
+ try {
343
+ pi.events.emit(MODEL_TASK_REQUEST_EVENT, { requestId });
344
+ }
345
+ finally {
346
+ off();
347
+ }
348
+ if (conflicts.size)
349
+ throw new Error(`Conflicting Model Task declarations: ${[...conflicts].join(", ")}.`);
350
+ return [...tasks.values()].sort((left, right) => left.id.localeCompare(right.id));
258
351
  }
259
352
  export function createTaskModelsExtension(pi, options) {
260
353
  const agentDir = options?.agentDir ?? getAgentDir();
@@ -288,25 +381,28 @@ export function createTaskModelsExtension(pi, options) {
288
381
  return false;
289
382
  }
290
383
  };
291
- const taskOptions = activeTaskPackages(pi, config.tasks).map((entry) => ({
292
- entry,
293
- label: `${entry.task} · ${config.tasks[entry.task]}`,
294
- }));
384
+ const taskOptions = discoverModelTasks(pi).map((task) => {
385
+ const profile = config.tasks[task.id] ?? task.defaultProfile;
386
+ return { task, label: `${task.label} · ${task.id} · ${profile}` };
387
+ });
295
388
  const selected = await ctx.ui.select("Task models", [
296
389
  ...profileOptions.map(({ label }) => label),
297
390
  ...taskOptions.map(({ label }) => label),
298
391
  ]);
299
392
  if (!selected)
300
393
  return;
301
- const task = taskOptions.find(({ label }) => label === selected)?.entry;
394
+ const task = taskOptions.find(({ label }) => label === selected)?.task;
302
395
  if (task) {
303
- const profile = await ctx.ui.select(`${task.task} profile`, [...PROFILE_NAMES]);
396
+ const profile = await ctx.ui.select(`${task.label}: ${task.purpose}`, [...PROFILE_NAMES]);
304
397
  if (!isProfileName(profile))
305
398
  return;
306
- config.tasks[task.task] = profile;
399
+ if (profile === task.defaultProfile)
400
+ delete config.tasks[task.id];
401
+ else
402
+ config.tasks[task.id] = profile;
307
403
  if (!save())
308
404
  return;
309
- ctx.ui.notify(`${task.task} assigned to ${profile}.`, "info");
405
+ ctx.ui.notify(`${task.id} assigned to ${profile}.`, "info");
310
406
  return;
311
407
  }
312
408
  const profile = profileOptions.find(({ label }) => label === selected)?.name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-task-models",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Shared task model profiles and routing for HenryQW Pi extensions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -35,8 +35,8 @@
35
35
  "pack:check": "npm pack --dry-run"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-ai": "^0.84.3",
39
- "@earendil-works/pi-coding-agent": "^0.84.3"
38
+ "@earendil-works/pi-ai": "^0.84.4",
39
+ "@earendil-works/pi-coding-agent": "^0.84.4"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",