@av-pi-studio/client 0.0.112 → 0.0.114

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.
@@ -0,0 +1,34 @@
1
+ import type { AgentTodosSnapshot } from "@av-pi-studio/protocol";
2
+ /**
3
+ * Framework-free `agent_todos_*` state (`swe/features/agent-todos.md` § SDK, sprint-079/task-008).
4
+ * No DOM, no I/O, no timers — mirrors `agent-ui-state.ts`'s pure-module convention.
5
+ *
6
+ * `seq` is per-agent, strictly increasing within one daemon process lifetime, and restarts at 1
7
+ * after a daemon restart. This module tracks the last-applied seq inline on the stored snapshot
8
+ * (there is no separate `lastSeq` field — `AgentTodosState` is exactly `{byAgent}`, per the task
9
+ * spec) and drops anything `seq <= lastSeq`. `resetAgentTodosSeq` lowers every stored seq to
10
+ * `-Infinity` so the *next* snapshot of any seq (including a fresh daemon's `1`) always wins,
11
+ * without discarding the previously-rendered tasks in the meantime. The consumer (sprint-080's
12
+ * store) calls it on every reconnect (new `server_info`) and immediately refetches via `list`, so
13
+ * the `-Infinity` seq is never itself rendered.
14
+ */
15
+ export interface AgentTodosState {
16
+ byAgent: Record<string, AgentTodosSnapshot>;
17
+ }
18
+ export declare const initialAgentTodosState: AgentTodosState;
19
+ /**
20
+ * Last-value-wins per agent. Ignores `seq <= lastSeq` for that agent (stale or duplicate
21
+ * delivery) and returns the **same** `state` object when ignored, so a React selector keyed off
22
+ * `state` reference never re-renders for a dropped push. A `list` response is applied through
23
+ * this same function — there is no separate code path for it.
24
+ */
25
+ export declare function applyAgentTodosSnapshot(state: AgentTodosState, snapshot: AgentTodosSnapshot): AgentTodosState;
26
+ /**
27
+ * Keeps every agent's last-known snapshot (tasks stay visible) but lowers its tracked seq to
28
+ * `-Infinity`, so the next `applyAgentTodosSnapshot` call for that agent wins regardless of its
29
+ * seq value. Call on every reconnect (new `server_info`), before refetching via `list`.
30
+ */
31
+ export declare function resetAgentTodosSeq(state: AgentTodosState): AgentTodosState;
32
+ /** The last-applied snapshot for one agent, or `undefined` if none has arrived yet. */
33
+ export declare function snapshotFor(state: AgentTodosState, agentId: string): AgentTodosSnapshot | undefined;
34
+ //# sourceMappingURL=agent-todos-state.d.ts.map
@@ -0,0 +1,32 @@
1
+ export const initialAgentTodosState = { byAgent: {} };
2
+ // ─── Reducer ──────────────────────────────────────────────────────────────────
3
+ /**
4
+ * Last-value-wins per agent. Ignores `seq <= lastSeq` for that agent (stale or duplicate
5
+ * delivery) and returns the **same** `state` object when ignored, so a React selector keyed off
6
+ * `state` reference never re-renders for a dropped push. A `list` response is applied through
7
+ * this same function — there is no separate code path for it.
8
+ */
9
+ export function applyAgentTodosSnapshot(state, snapshot) {
10
+ const current = state.byAgent[snapshot.agentId];
11
+ if (current && snapshot.seq <= current.seq)
12
+ return state;
13
+ return { byAgent: { ...state.byAgent, [snapshot.agentId]: snapshot } };
14
+ }
15
+ /**
16
+ * Keeps every agent's last-known snapshot (tasks stay visible) but lowers its tracked seq to
17
+ * `-Infinity`, so the next `applyAgentTodosSnapshot` call for that agent wins regardless of its
18
+ * seq value. Call on every reconnect (new `server_info`), before refetching via `list`.
19
+ */
20
+ export function resetAgentTodosSeq(state) {
21
+ const byAgent = {};
22
+ for (const [agentId, snapshot] of Object.entries(state.byAgent)) {
23
+ byAgent[agentId] = { ...snapshot, seq: Number.NEGATIVE_INFINITY };
24
+ }
25
+ return { byAgent };
26
+ }
27
+ // ─── Selectors ────────────────────────────────────────────────────────────────
28
+ /** The last-applied snapshot for one agent, or `undefined` if none has arrived yet. */
29
+ export function snapshotFor(state, agentId) {
30
+ return state.byAgent[agentId];
31
+ }
32
+ //# sourceMappingURL=agent-todos-state.js.map
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./pairing.js";
7
7
  export * from "./daemon-client.js";
8
8
  export * from "./pistudio-client.js";
9
9
  export * from "./agent-ui-state.js";
10
+ export * from "./agent-todos-state.js";
10
11
  export * from "./agent-ui-controller.js";
11
12
  export * from "./terminal-stream-router.js";
12
13
  export * from "./file-transfer-client.js";
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ export * from "./daemon-client.js";
15
15
  export * from "./pistudio-client.js";
16
16
  // Extension-UI pure state module (reducer/selectors, sprint-067).
17
17
  export * from "./agent-ui-state.js";
18
+ // Agent Todos pure state module (reducer/selectors, sprint-079).
19
+ export * from "./agent-todos-state.js";
18
20
  // Extension-UI controller (subscribe-then-list, reconnect resync, agent pruning, sprint-067).
19
21
  export * from "./agent-ui-controller.js";
20
22
  // Terminal-stream router (binary frame demux per slot).
@@ -1,4 +1,4 @@
1
- import type { AgentCloneResponse, AgentCompactResponse, AgentCycleModelResponse, AgentExportHtmlResponse, AgentForkMessagesResponse, AgentForkResponse, AgentLastAssistantTextResponse, AgentListCommandsResponse, AgentNewSessionResponse, AgentSessionStatsResponse, AgentSetModelResponse, AgentSetSessionNameResponse, AgentSetThinkingResponse, AgentStreamEvent, AgentSwitchSessionResponse, AgentThinkingLevelsResponse, AgentUiPendingRequest, AgentUiRequest, AgentUiResolved, AgentUiResponse, AgentUiSurface, CreateAgentRequest, ExtensionPacksListResponse, ExtensionPacksSetResponse, FetchAgentTimelineResponse, ProviderAuthInfo, ProviderAuthType, SessionMessage, SkillsInstallResponse, SkillsListResponse, SkillsPreviewResponse, SkillsRemoveResponse, SkillsSetEnabledResponse, SkillsUpdateAllResponse, TimelineDirection, ViewerSettings, ViewerSettingsPatchEntry } from "@av-pi-studio/protocol";
1
+ import type { AgentCloneResponse, AgentCompactResponse, AgentCycleModelResponse, AgentExportHtmlResponse, AgentForkMessagesResponse, AgentForkResponse, AgentLastAssistantTextResponse, AgentListCommandsResponse, AgentNewSessionResponse, AgentSessionStatsResponse, AgentSetModelResponse, AgentSetSessionNameResponse, AgentSetThinkingResponse, AgentStreamEvent, AgentSwitchSessionResponse, AgentThinkingLevelsResponse, AgentTodosSnapshot, AgentTodosUpdate, AgentUiPendingRequest, AgentUiRequest, AgentUiResolved, AgentUiResponse, AgentUiSurface, CreateAgentRequest, ExtensionPacksListResponse, ExtensionPacksSetResponse, FetchAgentTimelineResponse, ProviderAuthInfo, ProviderAuthType, SessionMessage, SkillsInstallResponse, SkillsListResponse, SkillsPreviewResponse, SkillsRemoveResponse, SkillsSetEnabledResponse, SkillsUpdateAllResponse, TimelineDirection, ViewerSettings, ViewerSettingsPatchEntry } from "@av-pi-studio/protocol";
2
2
  import type { DaemonClient } from "./daemon-client.js";
3
3
  export { detectSkillSource } from "@av-pi-studio/protocol";
4
4
  /**
@@ -362,6 +362,17 @@ export declare class AgentUiError extends Error {
362
362
  export declare function isAgentUiRequest(message: unknown): message is AgentUiRequest;
363
363
  /** Narrows an inbound session message to the `agent_ui_resolved` broadcast. */
364
364
  export declare function isAgentUiResolved(message: unknown): message is AgentUiResolved;
365
+ /**
366
+ * A domain failure reported *in `agent_todos_list_response`* (`{ok:false, error}`) rather than as
367
+ * an `rpc_error` — same hazard `ProviderAuthError`/`AgentUiError` guard against. `code` forwards
368
+ * the daemon's `error` string verbatim (open, not enumerated — only `not_found` is documented).
369
+ */
370
+ export declare class AgentTodosError extends Error {
371
+ readonly code: string;
372
+ constructor(message: string, code: string);
373
+ }
374
+ /** Narrows an inbound session message to the `agent_todos_update` broadcast. */
375
+ export declare function isAgentTodosUpdate(message: unknown): message is AgentTodosUpdate;
365
376
  /** The `agent_archived` lifecycle push (`messages.ts`'s `agentArchivedSchema`). */
366
377
  export interface AgentArchivedMessage {
367
378
  type: "agent_archived";
@@ -507,6 +518,22 @@ export declare class PiStudioClient {
507
518
  * from "does this daemon speak the `agent_ui_*` RPC family at all".
508
519
  */
509
520
  extensionUiAvailable(): boolean;
521
+ /** True iff the daemon advertised the `agentTodos` capability in `server_info.features`. An old
522
+ * daemon never gets `listAgentTodos`/`onAgentTodosUpdate` called against it. */
523
+ hasAgentTodosCapability(): boolean;
524
+ /**
525
+ * Reconnect/first-load snapshot for one agent's todo list — `agent_todos_list_request`, the
526
+ * default RPC timeout. The response is flat (`{type, requestId, snapshot}` on success, no
527
+ * `ok`), unlike most other RPCs on this facade: `daemon.request` only unwraps a `payload`
528
+ * field, and this family has none. A domain refusal (`{ok:false, error}`, e.g. unknown
529
+ * `agentId`) **throws** `AgentTodosError` — the caller treats any rejection as "keep what you
530
+ * have" (`agent-todos-state.ts`), so a malformed success (neither `snapshot` nor `ok:false`) is
531
+ * also raised rather than silently returning `undefined`.
532
+ */
533
+ listAgentTodos(agentId: string): Promise<AgentTodosSnapshot>;
534
+ /** Subscribe to every `agent_todos_update` broadcast (any agent) — raw pushes, undeduped and
535
+ * unfiltered by agent; feed them through `agent-todos-state.ts`'s `applyAgentTodosSnapshot`. */
536
+ onAgentTodosUpdate(handler: (snapshot: AgentTodosSnapshot) => void): () => void;
510
537
  /** True iff the daemon advertised the `skills` capability in `server_info.features`. */
511
538
  hasSkillsCapability(): boolean;
512
539
  /** Every resolved Agent Skill plus every resolved `settings.json` package — `skills_list_request`.
@@ -105,6 +105,24 @@ export function isAgentUiResolved(message) {
105
105
  typeof m.requestId === "string" &&
106
106
  typeof m.agentId === "string");
107
107
  }
108
+ /**
109
+ * A domain failure reported *in `agent_todos_list_response`* (`{ok:false, error}`) rather than as
110
+ * an `rpc_error` — same hazard `ProviderAuthError`/`AgentUiError` guard against. `code` forwards
111
+ * the daemon's `error` string verbatim (open, not enumerated — only `not_found` is documented).
112
+ */
113
+ export class AgentTodosError extends Error {
114
+ code;
115
+ constructor(message, code) {
116
+ super(message);
117
+ this.code = code;
118
+ this.name = "AgentTodosError";
119
+ }
120
+ }
121
+ /** Narrows an inbound session message to the `agent_todos_update` broadcast. */
122
+ export function isAgentTodosUpdate(message) {
123
+ const m = message;
124
+ return (!!m && m.type === "agent_todos_update" && typeof m.snapshot === "object" && m.snapshot !== null);
125
+ }
108
126
  /**
109
127
  * Narrows an inbound session message to `agent_archived`. **Not** part of the `agent_ui_*` family —
110
128
  * exported so a controller (task-003) can prune retained extension-UI state on archive/delete.
@@ -494,6 +512,41 @@ export class PiStudioClient {
494
512
  extensionUiAvailable() {
495
513
  return this.daemon.hasFeature("extensionUi");
496
514
  }
515
+ // ─── Agent Todos (agent_todos_*, sprint-079) ───────────────────────────────
516
+ /** True iff the daemon advertised the `agentTodos` capability in `server_info.features`. An old
517
+ * daemon never gets `listAgentTodos`/`onAgentTodosUpdate` called against it. */
518
+ hasAgentTodosCapability() {
519
+ return this.daemon.hasFeature("agentTodos");
520
+ }
521
+ /**
522
+ * Reconnect/first-load snapshot for one agent's todo list — `agent_todos_list_request`, the
523
+ * default RPC timeout. The response is flat (`{type, requestId, snapshot}` on success, no
524
+ * `ok`), unlike most other RPCs on this facade: `daemon.request` only unwraps a `payload`
525
+ * field, and this family has none. A domain refusal (`{ok:false, error}`, e.g. unknown
526
+ * `agentId`) **throws** `AgentTodosError` — the caller treats any rejection as "keep what you
527
+ * have" (`agent-todos-state.ts`), so a malformed success (neither `snapshot` nor `ok:false`) is
528
+ * also raised rather than silently returning `undefined`.
529
+ */
530
+ async listAgentTodos(agentId) {
531
+ const payload = await this.daemon.request("agent_todos_list_request", {
532
+ agentId,
533
+ });
534
+ if (payload.ok === false) {
535
+ throw new AgentTodosError(payload.error ?? "failed to list agent todos", payload.error ?? "unknown");
536
+ }
537
+ if (!payload.snapshot) {
538
+ throw new AgentTodosError("malformed agent_todos_list_response: missing snapshot", "malformed_response");
539
+ }
540
+ return payload.snapshot;
541
+ }
542
+ /** Subscribe to every `agent_todos_update` broadcast (any agent) — raw pushes, undeduped and
543
+ * unfiltered by agent; feed them through `agent-todos-state.ts`'s `applyAgentTodosSnapshot`. */
544
+ onAgentTodosUpdate(handler) {
545
+ return this.daemon.onSessionMessage((msg) => {
546
+ if (isAgentTodosUpdate(msg))
547
+ handler(msg.snapshot);
548
+ });
549
+ }
497
550
  // ─── Skills (skills_*, sprint-075) ─────────────────────────────────────────
498
551
  /** True iff the daemon advertised the `skills` capability in `server_info.features`. */
499
552
  hasSkillsCapability() {
@@ -47,10 +47,11 @@ export function makeScriptedDaemon(opts) {
47
47
  switch (msg.type) {
48
48
  case "agent_ui_respond_request":
49
49
  case "agent_ui_list_request":
50
+ case "agent_todos_list_request":
50
51
  // Driven manually by each test via `fake.push()` — the ok/error mix (and, for
51
- // agent_ui_list_request, the exact moment the reply lands relative to other pushed
52
- // events) is exactly what the acceptance criteria differentiate on; an automatic reply
53
- // would hide it.
52
+ // agent_ui_list_request/agent_todos_list_request, the exact moment the reply lands
53
+ // relative to other pushed events) is exactly what the acceptance criteria
54
+ // differentiate on; an automatic reply would hide it.
54
55
  return;
55
56
  case "create_agent_request": {
56
57
  const agentId = `agent-${++agentSeq}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@av-pi-studio/client",
3
- "version": "0.0.112",
3
+ "version": "0.0.114",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.map",
@@ -20,7 +20,12 @@
20
20
  "clean": "rm -rf dist *.tsbuildinfo"
21
21
  },
22
22
  "dependencies": {
23
- "@av-pi-studio/protocol": "^0.0.112",
24
- "@av-pi-studio/relay": "^0.0.112"
23
+ "@av-pi-studio/protocol": "^0.0.114",
24
+ "@av-pi-studio/relay": "^0.0.114"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/avatsaev/pi-studio.git",
29
+ "directory": "packages/client"
25
30
  }
26
31
  }