@amaster.ai/dsh-a2a 0.1.26 → 0.1.28

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
@@ -14,43 +14,42 @@ dsh plugin --profile my-agent add @amaster.ai/dsh-a2a
14
14
 
15
15
  ## Native inbox execution
16
16
 
17
- `inbox.enabled: true` opts into durable native DSH input instead of request-serialized execution. **This requires the unreleased upstream native inbox capability (`ctx.agents.nativeInboxVersion === 1`); the tested `0.2.0-rc.2` runtime does not provide it.** Startup rejects unsupported factories before opening a backend. Legacy execution remains the default.
17
+ `inbox.enabled: true` uses the official DSH inbox (`Agent.send`) for ordinary `next-turn` input and `next-step` steering. Tested with the unmodified npm runtime `0.2.1-rc.2`; no core patch is required. The queue and its durable projection belong to DSH. The plugin only materializes messages, attaches A2A task identities, and translates execution events.
18
18
 
19
- In this mode, ordinary `SendMessage` / `SendStreamingMessage` calls enter the DSH inbox before their initial task response is published. Nonblocking sends return a submitted task after durable admission. A context owns one agent, while tasks retain separate message identities, streams, and terminal states. A completed unit of work settles `completed`; use the same context with a new task for later work. Disconnecting an HTTP client does not cancel execution. Task snapshots, polling, and subscriptions use the standard A2A operations.
19
+ Standard `SendMessage` / `SendStreamingMessage`, task queries, cancellation and subscriptions remain the wire interface. The negotiated [inbox extension](../../docs/protocol/inbox-v1.md) forwards `target` and `wakeup` to DSH. `wakeup: false` leaves an idle driver parked; it does not prevent an active driver from consuming pending input. A later real waking message releases pending input.
20
20
 
21
- The optional [inbox extension](../../docs/protocol/inbox-v1.md) selects parked admission (`wakeup: false`) or next-step steering (`target: next-step`). Steering must address the currently active task. It cannot pull an unrelated task's input into the active model step.
21
+ The same-process `a2aInbox` service exposes `resolve(contextId)`, `content(message, contextId)`, `enqueue(taskId, message, { target?, wakeup? })`, `promote(contextId, nativeMessageId, activeTaskId)`, and `cancel(taskId)`. There is no separate pause, wake-only, resume, or retained-task restart operation. Cancellation is terminal and removes that task's pending input. Other tasks' input is retained according to official DSH cancellation semantics, and may wait for the next waking message. Clearing a context discards all its pending work.
22
22
 
23
- The same-process `a2aInbox` service implements the exported `A2aInbox` interface:
23
+ `a2a/before-followup` validates admission; `a2a/before-step` runs at the official pre-step boundary, after system-prompt assembly and before model execution. It cannot change the already assembled system prompt. Original request headers remain process-local. DSH persistence restores its own pending projection; the plugin does not replay splice logs or recover consumed claims. Graceful disposal follows the official runtime's cancellation behavior. Durable A2A task snapshots require a filesystem, Redis or GCS TaskStore; an in-memory store is sufficient without restart requirements.
24
+
25
+ ### Goal rounds
26
+
27
+ When the optional official `@deepseek-ai/dsh-goal` and `dsh-goal-round-driver` plugins are mounted, a Goal created or resumed during an A2A task keeps that task open while the Goal is active and armed. Automatic rounds retain the driver's original native message ID, content and `goal` source. The official driver still validates its reservation, revision and round; the plugin selects the owning task for streaming and `a2a/before-step` billing attribution. Editing the same Goal from another task preserves its original owner. Completing, pausing, blocking or disarming the Goal releases that ownership. A cold restart does not inherit automatic Goal authority: the host must explicitly resume it through a live attributed task.
28
+
29
+ ### Promote a queued input
30
+
31
+ Queue an ordinary future turn as a new task (omit `taskId`). To steer an active task with that already admitted input, pass its **native DSH message ID**, available from `agent.inbox.nextTurn` or `a2a/inbox` observations:
24
32
 
25
33
  ```ts
26
- const inbox = ctx.get('a2aInbox') as A2aInbox;
27
- const agent = await inbox.resolve(contextId);
28
- const blocks = await inbox.content(message, contextId);
29
- await inbox.enqueue(taskId, message); // Host follow-up with explicit task ownership.
30
- await inbox.pause(contextId);
31
- await inbox.wake(contextId); // Leaves a persisted pause intact.
32
- await inbox.wake(contextId, { resume: true }); // Explicitly unpauses existing work.
33
- await inbox.cancel(taskId, { retainInbox: true }); // Terminal task; pending input stays parked.
34
+ await ctx.a2aInbox.promote(contextId, nativeMessageId, activeTaskId);
34
35
  ```
35
36
 
36
- `pause` cancels active tasks while retaining their unconsumed input and other queued tasks; resumed input from a canceled task gets a fresh task identity. `CancelTask` remains terminal and removes that task's pending input, retaining other tasks' input. The service's explicit retention option parks the context; resuming retained input creates a new task when its former owner is terminal. `a2aTasks.clearContext` discards pending work even when idle. Plugin unload preserves the native inbox and drains its session writes. For restart recovery, configure durable DSH session persistence and a Redis or GCS TaskStore, then call `wake` for the contexts the host chooses to restore. `SubscribeToTask` observes recovered work without submitting a wake message.
37
+ Only an unclaimed, single-message independent task in the same context can be promoted. The native ID and A2A `messageId` stay unchanged; the target task owns the next-step input, and `referenceTaskIds` records the original task, which settles canceled after the durable transfer. Claimed input, a finished target, concurrent consumption or task settlement reject a stale request. A failed session checkpoint restores independent ownership; a later storage error can report failure after the transfer committed: inspect the queue and task states before sending new input. Recovery repairs interrupted transfers without replaying an ordinary DSH claim or committed model input.
37
38
 
38
- In native mode, host settlement hooks submit follow-ups through `a2aInbox.enqueue(taskId, message)`; it binds and persists the task identity before enqueueing. Raw `Agent.send` / `followup` inputs without A2A attribution are rejected rather than assigned to the most recent hook.
39
+ ### Local restart recovery
39
40
 
40
- `a2a/before-followup` validates admission; use `a2a/before-assembly` for per-input runtime state. The latter runs after the model-step batch is selected, before prompt assembly, and receives the same `A2aTurnContext` fields. Request headers are process-local: restored inputs receive `requestHeaders: undefined`. Persist replay-safe metadata in the message, never credentials in the queue. Admission observers remain non-vetoing.
41
+ Select `taskStore: filesystem` with `filesystem.root` pointing at a retained directory, and separately enable durable DSH session persistence. Keep queued file materialization on retained storage as well. Each task snapshot is scoped by tenant and user, written to a private temporary file and atomically renamed; no additional service is needed. Use one host writer per directory. Files have no automatic expiry; context clearing deletes task snapshots.
41
42
 
42
- Recovery only restores an interrupted claim without a committed `user/message`; it never replays committed model input or promises exactly-once tool effects. A task with committed input, no remaining pending input, and no durable terminal A2A checkpoint becomes failed on recovery, including when the turn ended before the process stopped. The host can then decide what to do next. Keep file parts on persistent storage with `uploadsDir` or `a2aFileMaterializer` when queued inputs must survive host or machine restarts. The memory TaskStore and temporary upload default do not provide that durability. In-process task handles remain until context clear or plugin unload; long-lived hosts should clear finished contexts.
43
+ After a crash, the host discovers sessions, calls `a2aInbox.resolve(contextId)` and supplies a real attributed waking input through the normal API. Restored unclaimed inputs keep their original task ownership. Startup scanning and scheduling remain host responsibilities. Explicit cancellation stays terminal and discards only the canceled task's input. This does not promise exactly-once external side effects, replay of consumed input, or graceful-unload retention beyond the official DSH lifecycle.
43
44
 
44
- Run the real loop / A2A HTTP / JSONL recovery checks against a prepared upstream checkout:
45
+ The dedicated `E2E (dsh-a2a native ownership / issue-75)` integration job runs these regressions with unmodified npm DSH and scripted model responses. Its Langfuse scenario verifies Goal rounds and promoted input under the original task trace, canceled source ownership, and execution restored after SIGKILL. Same-repository CI also sends the scenario to the configured real Langfuse project and polls its Observations API; fork PRs use the local collector without credentials.
45
46
 
46
- ```bash
47
- DSH_SOURCE_ROOT=/absolute/path/to/deepseek-harness node .github/scripts/check-native-inbox.mjs
47
+ ```sh
48
+ node .github/scripts/check-inbox.mjs -t issue-75
49
+ # With LANGFUSE_BASE_URL, LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY configured:
50
+ DSH_ISSUE75_LANGFUSE_LIVE=true node .github/scripts/check-inbox.mjs -t 'issue-75.*reports Goal continuation'
48
51
  ```
49
52
 
50
- The Integration workflow runs this suite without secrets, including on fork PRs, against a pinned upstream source checkout plus the checked-in native inbox source companion. It verifies insertion during an active turn, FIFO execution, independent task completion, and exactly one user-message commit per input.
51
-
52
- The checkout needs its installed dependencies and `pnpm run build:native-system`; model responses are scripted, so this check needs no gateway or model credentials. The upstream source companion is required before native mode can be released; no dependency artifact patch is installed by this plugin.
53
-
54
53
  ## Configuration
55
54
 
56
55
  Disabled by default. Configure via the profile's `cordis.patch.yml`:
@@ -80,7 +79,9 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
80
79
  description: My dsh agent over A2A
81
80
  version: 0.1.0
82
81
  publicUrl: https://agent.example.com
83
- taskStore: redis # memory | redis | gcs
82
+ taskStore: redis # memory | filesystem | redis | gcs
83
+ filesystem:
84
+ root: /srv/dsh/a2a-tasks # required when taskStore: filesystem
84
85
  redis:
85
86
  url: redis://127.0.0.1:6379
86
87
  keyPrefix: a2a
@@ -157,6 +158,7 @@ Forwarding follows the owning task's active request bus. Background child events
157
158
  A2A task snapshots retain protocol history and artifacts; [dsh-storage](../dsh-storage) separately records the full conversation. Backends save on task-state transitions, including the complete reply at `input-required`, so token-rate stream events never reach Redis or GCS. While a task is working, in-process history accumulates text deltas for `GetTask` and resubscription; a restart before the next state change can lose those transient deltas.
158
159
 
159
160
  - `memory` (default) — in-process, lost on restart
161
+ - `filesystem` — private, atomically replaced JSON snapshots under `filesystem.root`; no additional peer dependency
160
162
  - `redis` — task JSON under `<keyPrefix>:tasks:<scope>:<taskId>` with a TTL; requires the `ioredis` peer
161
163
  - `gcs` — gzipped task JSON at `<prefix>/<scope>/<taskId>/metadata.json.gz`; requires the `@google-cloud/storage` peer. `archiveWorkspace()` (tar of the workspace) exists but is not wired to the lifecycle yet.
162
164
 
@@ -168,7 +170,9 @@ dsh ships **no authentication or authorization**. The server binds `127.0.0.1` b
168
170
 
169
171
  ## Compatibility
170
172
 
171
- Pinned dsh/cordis versions live in the [root compat matrix](../../README.md#compatibility). Event payloads ride pre-release dsh APIs (`@deepseek-ai/dsh-{agent,session,llm,attachment,tools}@0.2.0-rc.2` — the attachment store is an optional peer) — the session peer floor is `>=0.2.0-rc.1` because child attribution uses `firstLifecycleSeq` to reject restored sessions; check the `TODO(verify)` markers in `src/` before upgrading dsh.
173
+ Optional Goal integration requires `@deepseek-ai/dsh-goal >=0.2.0-rc.2 || >=0.2.1-alpha.1` and the official Goal round driver (both tested at `0.2.1-rc.2`).
174
+
175
+ Pinned dsh/cordis versions live in the [root compat matrix](../../README.md#compatibility). Event payloads ride pre-release dsh APIs (`@deepseek-ai/dsh-{agent,session,llm,attachment,tools}@0.2.1-rc.2` — the attachment store is an optional peer) — the session peer floor is `>=0.2.0-rc.1` because child attribution uses `firstLifecycleSeq` to reject restored sessions; check the `TODO(verify)` markers in `src/` before upgrading dsh.
172
176
 
173
177
  ## License
174
178
 
package/lib/bridge.js CHANGED
@@ -191,14 +191,12 @@ export class A2aBridge {
191
191
  resumeSessionId: sessionId,
192
192
  agentOptions,
193
193
  setup,
194
- ...(this.inbox ? { keepInboxOnDispose: true } : {}),
195
194
  })
196
195
  : await this.ctx.agents.create({
197
196
  sessionId,
198
197
  meta: { cwd: this.options.cwd, ...(agentPreset ? { agentPreset } : {}) },
199
198
  agentOptions,
200
199
  setup,
201
- ...(this.inbox ? { keepInboxOnDispose: true } : {}),
202
200
  });
203
201
  const entry = {
204
202
  taskId,
@@ -299,7 +297,7 @@ export class A2aBridge {
299
297
  async waitForTurn(entry) {
300
298
  await Promise.allSettled([...entry.running]);
301
299
  }
302
- /** The native inbox selects task ownership before assembling its first prompt. */
300
+ /** The official inbox claim selects the task that owns the next model step. */
303
301
  bindInboxTask(entry, taskId, translator, bus, completion, controller) {
304
302
  this.tasks.delete(entry.taskId);
305
303
  entry.taskId = taskId;