@argszero/cordis-plugin-steer-preempt 0.1.0 → 0.1.2
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 +18 -2
- package/lib/index.js +23 -3
- package/lib/types/index.d.ts +25 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,8 +37,13 @@ a `tools/execute` around-wrapper (the same seam the in-tree
|
|
|
37
37
|
a real turn cancel keeps its semantics — the caller signal is never touched).
|
|
38
38
|
3. A monitor polls the agent's public `inbox.nextStep` queue (read-only — it
|
|
39
39
|
**never consumes**; the loop's `preStep` claim owns consumption). When a
|
|
40
|
-
steer has been pending for a short **settle grace** (default 1 s), it
|
|
41
|
-
the derived signal.
|
|
40
|
+
**user** steer has been pending for a short **settle grace** (default 1 s), it
|
|
41
|
+
aborts the derived signal. The trigger is a per-message
|
|
42
|
+
`message.source.kind === 'user'` test, **not** a bare queue-length check: the
|
|
43
|
+
`nextStep` queue also carries plugin-injected notifications (`agent.inject`,
|
|
44
|
+
e.g. a job-B completion notice while the model waits on job A, with
|
|
45
|
+
`source.kind === 'plugin'`). A plugin notice is not user input, so it must
|
|
46
|
+
not fire a user-steer preemption.
|
|
42
47
|
4. The abort makes `jobs.wait` reject (`'wait aborted'`); the wait logic
|
|
43
48
|
**uncounts the waiter but leaves the job running**. The registry converts the
|
|
44
49
|
tool throw into an error result.
|
|
@@ -81,6 +86,17 @@ No config is required. Optional tuning (via a `set` layer):
|
|
|
81
86
|
| `pollMs` | `200` | Poll interval for the pending next-step queue while a blocking read is active |
|
|
82
87
|
| `graceMs` | `1000` | Settle grace: after a steer is observed, how long to let a nearly-done job finish before aborting |
|
|
83
88
|
|
|
89
|
+
## Changelog
|
|
90
|
+
|
|
91
|
+
- **v0.1.1**: preemption trigger narrowed from `nextStep.length > 0` to a
|
|
92
|
+
per-message `message.source.kind === 'user'` test. Fixes a false preemption
|
|
93
|
+
when a plugin-injected notification (`agent.inject`, e.g. a job-B completion
|
|
94
|
+
notice with `source.kind === 'plugin'`) sits in the same `nextStep` queue as a
|
|
95
|
+
real user steer. Regression tests added. (Community feedback, discussion
|
|
96
|
+
#6030.)
|
|
97
|
+
- **v0.1.0**: initial release — preempt a blocking `job_output(wait: true)` read
|
|
98
|
+
when a user steer is queued.
|
|
99
|
+
|
|
84
100
|
## What it does not do
|
|
85
101
|
|
|
86
102
|
- It does **not** kill or cancel the background job — the job keeps running and
|
package/lib/index.js
CHANGED
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
* for the duration of the dispatch, polls the agent's public
|
|
17
17
|
* `inbox.nextStep` queue (read-only — it never consumes anything; the loop's
|
|
18
18
|
* `preStep` claim owns consumption), and aborts the derived signal once a
|
|
19
|
-
* pending steer has survived a short settle grace. The abort makes the
|
|
20
|
-
* wait return early — the background job is **not** killed (the wait logic
|
|
19
|
+
* pending **user** steer has survived a short settle grace. The abort makes the
|
|
20
|
+
* tool wait return early — the background job is **not** killed (the wait logic
|
|
21
21
|
* uncounts the waiter and rejects; the job keeps running) — so the step ends,
|
|
22
22
|
* the loop reaches its step boundary, and the queued steer is honored promptly.
|
|
23
23
|
*
|
|
24
|
+
* Refinement (v0.1.1, #6030 feedback): the `nextStep` queue is shared by two
|
|
25
|
+
* producers — `agent.steer(input)` (user-initiated, `send('next-step', true)`)
|
|
26
|
+
* and `agent.inject(input)` (plugin-initiated notification, e.g. a job-B
|
|
27
|
+
* completion notice while the model waits on job A; `send('next-step', false)`).
|
|
28
|
+
* `Agent.inbox.nextStep` cannot distinguish them, so the plugin must filter by
|
|
29
|
+
* `message.source.kind === 'user'`. Counting `nextStep.length` alone would
|
|
30
|
+
* falsely preempt a job-A wait when a job-B completion notice is injected into
|
|
31
|
+
* the same queue — but that notice is not user input, so no user-steer abort
|
|
32
|
+
* should fire. The trigger is now a per-message `source.kind === 'user'` test.
|
|
33
|
+
*
|
|
24
34
|
* Mechanism notes (verified against packages/core/tools/src/index.ts,
|
|
25
35
|
* packages/jobs/jobs-local/src/index.ts, packages/jobs/tool-jobs/src/index.ts
|
|
26
36
|
* and packages/core/agent/src/runtime-types.ts on dsh 0.1.5-alpha.1):
|
|
@@ -88,6 +98,16 @@ function preemptedResult(jobId) {
|
|
|
88
98
|
function sleep(ms) {
|
|
89
99
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
90
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* True when the pending next-step queue contains a *user-initiated* steer
|
|
103
|
+
* (`message.source.kind === 'user'`). Plugin-injected notifications (job
|
|
104
|
+
* completion notices, `source.kind === 'plugin'`) share the same queue but must
|
|
105
|
+
* NOT trigger a user-steer preemption — a second job finishing while the model
|
|
106
|
+
* waits on first job is not user input.
|
|
107
|
+
*/
|
|
108
|
+
function hasUserSteer(queue) {
|
|
109
|
+
return queue.some(message => message.source?.kind === 'user');
|
|
110
|
+
}
|
|
91
111
|
/**
|
|
92
112
|
* Watch the agent's pending next-step queue and abort `controller` once a
|
|
93
113
|
* steer has been pending for `graceMs` (polling every `pollMs`). `isDone`
|
|
@@ -133,7 +153,7 @@ export async function runWrapper(config, exec, next) {
|
|
|
133
153
|
const controller = new AbortController();
|
|
134
154
|
let done = false;
|
|
135
155
|
const isDone = () => done;
|
|
136
|
-
const hasPendingSteer = () => nextStep
|
|
156
|
+
const hasPendingSteer = () => hasUserSteer(nextStep);
|
|
137
157
|
void monitorSteers(hasPendingSteer, controller, config.pollMs, config.graceMs, isDone);
|
|
138
158
|
const upstream = exec.signal;
|
|
139
159
|
exec.signal = controller.signal;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
* for the duration of the dispatch, polls the agent's public
|
|
17
17
|
* `inbox.nextStep` queue (read-only — it never consumes anything; the loop's
|
|
18
18
|
* `preStep` claim owns consumption), and aborts the derived signal once a
|
|
19
|
-
* pending steer has survived a short settle grace. The abort makes the
|
|
20
|
-
* wait return early — the background job is **not** killed (the wait logic
|
|
19
|
+
* pending **user** steer has survived a short settle grace. The abort makes the
|
|
20
|
+
* tool wait return early — the background job is **not** killed (the wait logic
|
|
21
21
|
* uncounts the waiter and rejects; the job keeps running) — so the step ends,
|
|
22
22
|
* the loop reaches its step boundary, and the queued steer is honored promptly.
|
|
23
23
|
*
|
|
24
|
+
* Refinement (v0.1.1, #6030 feedback): the `nextStep` queue is shared by two
|
|
25
|
+
* producers — `agent.steer(input)` (user-initiated, `send('next-step', true)`)
|
|
26
|
+
* and `agent.inject(input)` (plugin-initiated notification, e.g. a job-B
|
|
27
|
+
* completion notice while the model waits on job A; `send('next-step', false)`).
|
|
28
|
+
* `Agent.inbox.nextStep` cannot distinguish them, so the plugin must filter by
|
|
29
|
+
* `message.source.kind === 'user'`. Counting `nextStep.length` alone would
|
|
30
|
+
* falsely preempt a job-A wait when a job-B completion notice is injected into
|
|
31
|
+
* the same queue — but that notice is not user input, so no user-steer abort
|
|
32
|
+
* should fire. The trigger is now a per-message `source.kind === 'user'` test.
|
|
33
|
+
*
|
|
24
34
|
* Mechanism notes (verified against packages/core/tools/src/index.ts,
|
|
25
35
|
* packages/jobs/jobs-local/src/index.ts, packages/jobs/tool-jobs/src/index.ts
|
|
26
36
|
* and packages/core/agent/src/runtime-types.ts on dsh 0.1.5-alpha.1):
|
|
@@ -81,11 +91,23 @@ export interface WrapperExec {
|
|
|
81
91
|
readonly arguments: unknown;
|
|
82
92
|
readonly agent?: {
|
|
83
93
|
readonly inbox?: {
|
|
84
|
-
readonly nextStep?: readonly
|
|
94
|
+
readonly nextStep?: readonly UserMessageLike[];
|
|
85
95
|
};
|
|
86
96
|
};
|
|
87
97
|
signal: AbortSignal;
|
|
88
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Minimal structural view of a queued next-step entry. Only `source.kind` is
|
|
101
|
+
* inspected: a user-initiated steer carries `source.kind === 'user'`; a
|
|
102
|
+
* plugin-injected notification (e.g. a job completion notice) carries
|
|
103
|
+
* `source.kind === 'plugin'`. This is how the plugin distinguishes a real user
|
|
104
|
+
* steer from an injected context artifact sharing the same queue.
|
|
105
|
+
*/
|
|
106
|
+
export interface UserMessageLike {
|
|
107
|
+
readonly source?: {
|
|
108
|
+
readonly kind?: string;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
89
111
|
/** Structural view of the delegated dispatch (`next()`). */
|
|
90
112
|
export type WrapperNext = () => Promise<ToolExecutionResult>;
|
|
91
113
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@argszero/cordis-plugin-steer-preempt",
|
|
3
3
|
"description": "Steer preemption for dsh: a new user input (next-step steer) interrupts a blocking job_output(wait: true) read immediately instead of letting it block up to the job-wait cap, so the agent loop reaches its step boundary and honors the steer. The background job is never killed. Compatible with dsh 0.1.5-alpha.1 (job_output wait + Agent.inbox.nextStep).",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
41
|
-
"@deepseek-ai/dsh-tools": ">=0.1.2"
|
|
41
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@deepseek-ai/schemastery": "^3.18.1"
|