@stackstackstack/dsh-goal-round-driver 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +64 -0
- package/README.zh.md +64 -0
- package/lib/index.js +363 -0
- package/lib/invariant.js +77 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/prompt.d.ts +11 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/goal/goal-round-driver/README.md
|
|
5
|
+
README.md: ac4bc4365ad2a09938e0166e939ac23ebff8dbbd
|
|
6
|
+
README.zh.md: daa8eee9f03b0082a0a229042057ad306080a5d6
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @stackstackstack/dsh-goal-round-driver
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Same-session continuation driver for [`ctx.goals`](../goal/README.md). It turns an active, armed goal into sequential [goal rounds](../../../docs/glossary.md#goal-round) through the public `Agent` and session services; the [same-session driver Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md) owns the race and lifecycle rationale.
|
|
6
|
+
|
|
7
|
+
## Composition
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: goal
|
|
11
|
+
name: '@stackstackstack/dsh-goal'
|
|
12
|
+
|
|
13
|
+
- id: tool-goal
|
|
14
|
+
name: '@stackstackstack/dsh-tool-goal'
|
|
15
|
+
|
|
16
|
+
- id: goal-round-driver
|
|
17
|
+
name: '@stackstackstack/dsh-goal-round-driver'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The plugin has no tunable configuration. `maxGoalRounds` belongs to the goal definition, while the model-facing blocked threshold belongs to [`dsh-tool-goal`](../tool-goal/README.md); duplicating either value in the driver could produce divergent policy.
|
|
21
|
+
|
|
22
|
+
## Round contract
|
|
23
|
+
|
|
24
|
+
When an exact live agent is idle with an active, armed goal and remaining capacity, the driver first checkpoints pending goal mutations, then reserves `roundsStarted + 1` for the current `{ goalId, revision }`. It queues one `<goal_round>` prompt with `GoalMessageSource`. The `agent/pre-step` listener verifies the complete claimed record and current goal both before and after downstream listeners; only an entered `user/message` increments `roundsStarted`. A reservation rejected as stale does not consume the round number.
|
|
25
|
+
|
|
26
|
+
`MessageId` identifies the reserved message through durable inbox insertion and claim; it does not identify a turn result. Human messages do not consume the goal cap. If human work enters the inbox before a reservation or joins its pending batch, automatic work yields until the agent becomes idle; a pending automatic prompt in a mixed batch is rejected and re-reserved only after that checkpoint.
|
|
27
|
+
|
|
28
|
+
The retained prompt names the JSON-quoted objective and `round/maxGoalRounds`, treats the current workspace, tool results, and durable session state as authoritative, requires evidence before completion, and tells the model to leave the goal active when work remains. Quoting preserves multiline or tag-like objective text as data. Goal lifecycle mutations still require the independent authority checks in `dsh-tool-goal`.
|
|
29
|
+
|
|
30
|
+
## Idle checkpoint
|
|
31
|
+
|
|
32
|
+
At whole-agent idle, durable goal phase and revision are authoritative. An active, armed goal with capacity reserves its next round; completion, pause, blocking, and edits suppress continuation. The driver does not classify the preceding activity by correlating the goal message with `turn/end`, so provider errors and token limits are not prompt-level goal outcomes.
|
|
33
|
+
|
|
34
|
+
## Lifecycle and durability
|
|
35
|
+
|
|
36
|
+
`goal/changed` creates a durability obligation. Before queuing work, the driver awaits `ctx.sessions.flush()` and rechecks both the goal revision and competing input after the await. A flush failure arriving through `agent/error` disarms continuation before another round can start.
|
|
37
|
+
|
|
38
|
+
Activation is never inherited when this plugin loads over an existing agent. `GoalService.disarm()` removes process-local authority without changing durable phase, revision, or history; explicit human-authorized resume records the later reactivation. The same rule applies after session resume and fork through the goal domain's `agent/session-start` handling.
|
|
39
|
+
|
|
40
|
+
Cancellation removes pending inbox work or leaves an agent-wide aborted state. At the next idle checkpoint the driver pauses a goal with a reserved or admitted attempt so cancellation cannot auto-restart it; cancellation unrelated to a goal attempt only disarms process-local continuation. If the pause mutation fails, the driver falls back to disarming. Plugin teardown closes admission, disarms every live goal, cancels active work with the `parent` cause, and awaits the driver plus agent quiescence while its event fence remains installed.
|
|
41
|
+
|
|
42
|
+
## Model Experience
|
|
43
|
+
|
|
44
|
+
### Goal-round prompt
|
|
45
|
+
|
|
46
|
+
#### What the model sees
|
|
47
|
+
|
|
48
|
+
Each admitted round is one retained user-role `<goal_round>` block naming the full objective and positive round number. Earlier human messages, goal-state snapshots, assistant output, and tool records remain in the same session history.
|
|
49
|
+
|
|
50
|
+
#### Token effect
|
|
51
|
+
|
|
52
|
+
One fixed instruction block plus the objective is added per admitted round. Later requests resend retained rounds until compaction shadows them; no fresh agent or copied conversation prefix is created.
|
|
53
|
+
|
|
54
|
+
#### KV Cache effect
|
|
55
|
+
|
|
56
|
+
Append-only within an epoch: each admitted round extends the existing conversation after its reusable prefix. Compaction may replace the derived-history suffix and move the reusable boundary.
|
|
57
|
+
|
|
58
|
+
## Known Limitations and Deferred Work
|
|
59
|
+
|
|
60
|
+
- **No independent evaluator** — the model-facing goal policy decides when evidence is sufficient for completion and whether a blocker is semantically unchanged; evaluator-backed certification remains deferred.
|
|
61
|
+
- **Same-session execution only** — this package deliberately does not spawn a fresh agent, fork a session prefix, or implement Ralph-style independent attempts; that workflow belongs to its own plugin layer.
|
|
62
|
+
- **Accepted-queue unload race** — Cordis plugin unload is asynchronous. A goal prompt already accepted by the agent inbox can begin and consume its round before unload starts; teardown then cancels the request, disarms the goal, and awaits quiescence. No later round starts.
|
|
63
|
+
- **Round cap, not resource budget** — token, currency, time, and provider quota policies remain independent. Their session events are not attributed to the goal message or mapped into goal blocker codes.
|
|
64
|
+
- **No abnormal auto-retry** — transient provider and persistence failures require a later human-authorized resume rather than an implicit retry policy.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @stackstackstack/dsh-goal-round-driver
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
[`ctx.goals`](../goal/README.md) 的同会话续行驱动器。它通过公开 `Agent` 与会话服务,把 phase 为 active 且已启用续行的目标转换为连续的 [Goal Round](../../../docs/glossary.md#goal-round);[同会话驱动器 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md) 记载竞态与生命周期方面的设计理由。
|
|
6
|
+
|
|
7
|
+
## 组合
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: goal
|
|
11
|
+
name: '@stackstackstack/dsh-goal'
|
|
12
|
+
|
|
13
|
+
- id: tool-goal
|
|
14
|
+
name: '@stackstackstack/dsh-tool-goal'
|
|
15
|
+
|
|
16
|
+
- id: goal-round-driver
|
|
17
|
+
name: '@stackstackstack/dsh-goal-round-driver'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
该插件没有可调配置。`maxGoalRounds` 属于目标定义,面向模型的阻塞阈值则属于 [`dsh-tool-goal`](../tool-goal/README.md);在驱动器中重复任一数值都可能产生分歧策略。
|
|
21
|
+
|
|
22
|
+
## Round 约定
|
|
23
|
+
|
|
24
|
+
当对应的活跃 agent(智能体)实例处于 idle 状态,且目标 phase 为 active、已启用续行并有剩余容量时,驱动器先为待处理 goal 变更创建检查点,再预留 `roundsStarted + 1`,对应当前 `{ goalId, revision }`。它会排入一条 `<goal_round>` 提示词,并携带 `GoalMessageSource`。`agent/pre-step` 监听器会在下游监听器前后验证完整的已领取记录与当前 goal;只有进入步骤的 `user/message` 才会增加 `roundsStarted`。因陈旧而被拒绝的预留不会消耗 Round 编号。
|
|
25
|
+
|
|
26
|
+
`MessageId` 通过持久 inbox 插入和领取来标识预留消息;它不标识轮次结果。人类消息不消耗 goal 上限。如果人类工作在预留前进入 inbox,或加入预留的待处理批次,自动工作会让行,直到 agent 进入 idle;混合批次中的待处理自动提示词会被拒绝,只有在该检查点之后才重新预留。
|
|
27
|
+
|
|
28
|
+
保留的提示词会点明经过 JSON 引用的目标与 `round/maxGoalRounds`,将当前工作区、工具结果和持久会话状态视为权威信息,要求在完成前提供证据,并要求在工作仍未完成时保持目标 active。引用可将多行或形似标签的目标文本保留为数据。goal 生命周期变更仍必须通过 `dsh-tool-goal` 的独立权限检查。
|
|
29
|
+
|
|
30
|
+
## Idle 检查点
|
|
31
|
+
|
|
32
|
+
整个 agent 进入 idle 时,持久 goal phase 和 revision 具有权威性。phase 为 active、已启用续行且仍有容量的 goal 会预留下一 Round;完成、暂停、阻塞和编辑都会阻止续行。驱动器不会通过关联 goal 消息与 `turn/end` 来对前一段活动分类,因此提供方错误和 token 上限不属于提示词级 goal 结果。
|
|
33
|
+
|
|
34
|
+
## 生命周期与持久性
|
|
35
|
+
|
|
36
|
+
`goal/changed` 会产生持久性义务。排队工作前,驱动器会等待 `ctx.sessions.flush()`,并在等待后重新检查 goal revision 与竞争输入。通过 `agent/error` 到达的 flush 失败会停用续行,避免另一 Round 启动。
|
|
37
|
+
|
|
38
|
+
此插件加载到现有 agent 上时绝不会继承续行启用状态。`GoalService.disarm()` 会移除进程本地权限,而不改变持久 phase、revision 或历史;之后由用户明确授权的 resume 会记录重新启用续行。会话 resume 和 fork 后,goal 领域通过 `agent/session-start` 处理应用相同规则。
|
|
39
|
+
|
|
40
|
+
取消会移除 inbox 中待处理的工作,或留下 agent 范围的 aborted 状态。在下一次 idle 检查点,驱动器会暂停存在已预留或已准入尝试的 goal,避免取消后自动重启;与 goal 尝试无关的取消只会撤销进程本地续行权限。如果 pause 变更失败,驱动器会回退到停用续行。插件 teardown 会关闭准入,停用所有活跃 goal 的续行,以 `parent` cause 取消正在进行的工作,并在事件防护仍生效的情况下等待驱动器和 agent 完全停稳。
|
|
41
|
+
|
|
42
|
+
## 模型体验
|
|
43
|
+
|
|
44
|
+
### Goal Round 提示词
|
|
45
|
+
|
|
46
|
+
#### 模型看到的内容
|
|
47
|
+
|
|
48
|
+
每个已准入 Round 都是一段保留的用户角色 `<goal_round>` 块,其中点明完整目标与正数 Round 编号。更早的用户消息、goal 状态快照、assistant 输出与工具记录仍保留在同一会话历史中。
|
|
49
|
+
|
|
50
|
+
#### Token 影响
|
|
51
|
+
|
|
52
|
+
每个已准入 Round 会增加一个固定指令块和目标。后续请求会重新发送保留的 Round,直到压缩(compaction)将其遮蔽;不会创建新 agent,也不会复制对话前缀。
|
|
53
|
+
|
|
54
|
+
#### KV Cache 影响
|
|
55
|
+
|
|
56
|
+
在一个 epoch 内仅追加:每个已准入 Round 都会在可复用前缀后扩展现有对话。压缩可能替换派生历史后缀,并移动可复用边界。
|
|
57
|
+
|
|
58
|
+
## 已知限制与暂缓事项
|
|
59
|
+
|
|
60
|
+
- **没有独立评估器**:面向模型的 goal 策略会判断证据是否足以完成,以及 blocker 在语义上是否未变;评估器支持的认证仍保持暂缓。
|
|
61
|
+
- **只在同一会话执行**:此包有意不 spawn 新 agent、不 fork 会话前缀,也不实现 Ralph 风格的独立尝试;该工作流属于单独的插件层。
|
|
62
|
+
- **已接受队列的卸载竞态**:Cordis 插件卸载是异步的。已经被 agent inbox 接受的 goal 提示词可以在卸载开始前启动并消耗其 Round;teardown 随后会取消请求、停用 goal 的续行并等待完全停稳。不会再启动后续 Round。
|
|
63
|
+
- **只有 Round 上限,不是资源预算**:token、货币、时间与提供方配额策略保持独立。对应的会话事件不会归属于 goal 消息,也不会映射为 goal 阻塞代码。
|
|
64
|
+
- **异常情况不自动重试**:暂时性的提供方与持久化失败需要之后由用户授权 resume,而不会采用隐式重试策略。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { createUserMessage } from "@stackstackstack/dsh-llm";
|
|
3
|
+
//#region lib/types/prompt.js
|
|
4
|
+
/** Model-visible continuation prompt for one same-session goal round. */
|
|
5
|
+
/**
|
|
6
|
+
* Render the complete goal-round instruction retained in session history.
|
|
7
|
+
* @param goal - exact active goal revision being admitted.
|
|
8
|
+
* @param round - next positive round number.
|
|
9
|
+
* @returns a fresh one-block prompt for `Agent.followup()`.
|
|
10
|
+
*/
|
|
11
|
+
function renderGoalRoundPrompt(goal, round) {
|
|
12
|
+
return [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: `<goal_round>
|
|
15
|
+
Objective: ${JSON.stringify(goal.objective)}\nRound: ${round}/${goal.maxGoalRounds}\n\nContinue working toward the objective in this same session. Treat the current workspace, tool results, and durable session state as authoritative; inspect them instead of assuming earlier narration is still current. Make concrete progress and verify the result. Before claiming completion, gather evidence that the whole objective is achieved, read the current goal, and mark it complete. If work remains, leave the goal active for the next round. Follow the configured goal-tool policy before reporting a blocker.
|
|
16
|
+
</goal_round>`
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/types/index.js
|
|
21
|
+
/**
|
|
22
|
+
* Same-session goal-round driver over public agent, session, and goal services.
|
|
23
|
+
* @module @stackstackstack/dsh-goal-round-driver
|
|
24
|
+
*/
|
|
25
|
+
const name = "goal-round-driver";
|
|
26
|
+
const inject = [
|
|
27
|
+
"agents",
|
|
28
|
+
"goals",
|
|
29
|
+
"sessions"
|
|
30
|
+
];
|
|
31
|
+
/** Whether a source identifies an automatic, positive-numbered goal round. */
|
|
32
|
+
function isGoalRoundSource(source) {
|
|
33
|
+
return source.kind === "goal" && source.round > 0;
|
|
34
|
+
}
|
|
35
|
+
/** Compare a source to one reserved identity. */
|
|
36
|
+
function sameRound(source, round) {
|
|
37
|
+
return source.goalId === round.goalId && source.revision === round.revision && source.round === round.round;
|
|
38
|
+
}
|
|
39
|
+
/** Compare the complete queued record to the driver's reservation. */
|
|
40
|
+
function sameQueued(content, source, attempt) {
|
|
41
|
+
return isGoalRoundSource(source) && sameRound(source, attempt) && isDeepStrictEqual(content, attempt.content);
|
|
42
|
+
}
|
|
43
|
+
/** Exact current ref for a view. */
|
|
44
|
+
function goalRef(goal) {
|
|
45
|
+
return {
|
|
46
|
+
id: goal.id,
|
|
47
|
+
revision: goal.revision
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Human-readable unexpected values for logs. */
|
|
51
|
+
function renderThrown(value) {
|
|
52
|
+
return value instanceof Error ? value.message : String(value);
|
|
53
|
+
}
|
|
54
|
+
/** Install automatic same-session continuation and its race fences. */
|
|
55
|
+
function apply(ctx) {
|
|
56
|
+
const states = /* @__PURE__ */ new Map();
|
|
57
|
+
/** Create state for an exact currently live agent. */
|
|
58
|
+
function stateFor(agent) {
|
|
59
|
+
const existing = states.get(agent);
|
|
60
|
+
if (existing !== void 0) return existing;
|
|
61
|
+
const state = {
|
|
62
|
+
agent,
|
|
63
|
+
attempt: void 0,
|
|
64
|
+
competingQueued: false,
|
|
65
|
+
needsCheckpoint: false,
|
|
66
|
+
requested: false,
|
|
67
|
+
run: void 0,
|
|
68
|
+
stopping: false
|
|
69
|
+
};
|
|
70
|
+
states.set(agent, state);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
/** Read only when the exact Agent remains live. */
|
|
74
|
+
function currentGoal(state) {
|
|
75
|
+
if (ctx.agents.get(state.agent.id) !== state.agent) return void 0;
|
|
76
|
+
return ctx.goals.get(state.agent);
|
|
77
|
+
}
|
|
78
|
+
/** Whether this exact lifecycle is quiescent with no competing prompt. */
|
|
79
|
+
function readyToDrive(state) {
|
|
80
|
+
return ctx.fiber.state === 2 && !state.stopping && ctx.agents.get(state.agent.id) === state.agent && state.agent.status === "idle" && !state.competingQueued;
|
|
81
|
+
}
|
|
82
|
+
/** Recheck every condition that an awaited checkpoint may have changed. */
|
|
83
|
+
function readyAfterCheckpoint(state) {
|
|
84
|
+
return readyToDrive(state) && !state.needsCheckpoint;
|
|
85
|
+
}
|
|
86
|
+
/** Remove automatic authority while preserving the durable phase. */
|
|
87
|
+
function disarm(state) {
|
|
88
|
+
try {
|
|
89
|
+
if (currentGoal(state)?.activation === "armed") ctx.goals.disarm(state.agent);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
ctx.logger.warn(`goal-round-driver: could not disarm agent "${state.agent.id}": ${renderThrown(error)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Preserve claimed step context when this driver drops only its own round. */
|
|
95
|
+
function restoreOtherClaimed(agent, messages, messageId) {
|
|
96
|
+
const retained = messages.filter((message) => message.id !== messageId && !(message.source.kind === "goal" && message.source.round === 0));
|
|
97
|
+
for (const message of retained.toReversed()) {
|
|
98
|
+
if (agent.inbox.nextStep.some((candidate) => candidate.id === message.id) || agent.inbox.nextTurn.some((candidate) => candidate.id === message.id)) continue;
|
|
99
|
+
agent.inbox.prepend("next-step", message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Process admitted work at quiescence, then reserve at most one next round. */
|
|
103
|
+
async function drive(state) {
|
|
104
|
+
const { agent } = state;
|
|
105
|
+
if (!readyToDrive(state)) return;
|
|
106
|
+
if (state.needsCheckpoint) {
|
|
107
|
+
state.needsCheckpoint = false;
|
|
108
|
+
try {
|
|
109
|
+
await ctx.sessions.flush(agent.session);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
ctx.logger.warn(`goal-round-driver: durability checkpoint failed for agent "${agent.id}": ${renderThrown(error)}`);
|
|
112
|
+
disarm(state);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!readyAfterCheckpoint(state)) return;
|
|
116
|
+
}
|
|
117
|
+
if (state.attempt !== void 0) {
|
|
118
|
+
state.attempt = void 0;
|
|
119
|
+
state.needsCheckpoint = true;
|
|
120
|
+
state.requested = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const goal = currentGoal(state);
|
|
124
|
+
if (goal === void 0 || goal.phase !== "active" || goal.activation !== "armed") return;
|
|
125
|
+
if (goal.roundsStarted >= goal.maxGoalRounds) {
|
|
126
|
+
ctx.goals.block(agent, goalRef(goal), {
|
|
127
|
+
code: "round-limit",
|
|
128
|
+
message: `Goal reached its configured limit of ${goal.maxGoalRounds} rounds.`
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const round = goal.roundsStarted + 1;
|
|
133
|
+
const content = renderGoalRoundPrompt(goal, round);
|
|
134
|
+
const message = createUserMessage({
|
|
135
|
+
content,
|
|
136
|
+
source: {
|
|
137
|
+
kind: "goal",
|
|
138
|
+
goalId: goal.id,
|
|
139
|
+
revision: goal.revision,
|
|
140
|
+
round
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
state.attempt = {
|
|
144
|
+
goalId: goal.id,
|
|
145
|
+
revision: goal.revision,
|
|
146
|
+
round,
|
|
147
|
+
messageId: message.id,
|
|
148
|
+
content,
|
|
149
|
+
phase: "queued",
|
|
150
|
+
cancelled: false,
|
|
151
|
+
stale: false
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
agent.followup(message);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
state.attempt = void 0;
|
|
157
|
+
ctx.logger.warn(`goal-round-driver: could not queue round ${round} for agent "${agent.id}": ${renderThrown(error)}`);
|
|
158
|
+
const latest = currentGoal(state);
|
|
159
|
+
if (latest !== void 0 && latest.id === goal.id && latest.revision === goal.revision && latest.phase === "active" && latest.activation === "armed") ctx.goals.block(agent, goalRef(latest), {
|
|
160
|
+
code: "queue-failed",
|
|
161
|
+
message: `Could not queue goal round ${round}: ${renderThrown(error)}`
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Coalesce triggers onto one agent-local serialized driver. */
|
|
166
|
+
function requestDrive(state) {
|
|
167
|
+
/* v8 ignore next -- teardown may race a final trigger after synchronously closing the step fence */
|
|
168
|
+
if (state.stopping) return;
|
|
169
|
+
state.requested = true;
|
|
170
|
+
if (state.run !== void 0) return;
|
|
171
|
+
let run;
|
|
172
|
+
try {
|
|
173
|
+
run = ctx.agents.withoutInitiator(async () => {
|
|
174
|
+
while (state.requested && !state.stopping) {
|
|
175
|
+
state.requested = false;
|
|
176
|
+
try {
|
|
177
|
+
await drive(state);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
ctx.logger.warn(`goal-round-driver: driver failed for agent "${state.agent.id}": ${renderThrown(error)}`);
|
|
180
|
+
disarm(state);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.logger.warn(`goal-round-driver: could not start driver for agent "${state.agent.id}": ${renderThrown(error)}`);
|
|
186
|
+
disarm(state);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
state.run = run;
|
|
190
|
+
const retire = () => {
|
|
191
|
+
state.run = void 0;
|
|
192
|
+
if (state.requested && !state.stopping) requestDrive(state);
|
|
193
|
+
};
|
|
194
|
+
run.then(retire, (error) => {
|
|
195
|
+
ctx.logger.warn(`goal-round-driver: driver task rejected for agent "${state.agent.id}": ${renderThrown(error)}`);
|
|
196
|
+
disarm(state);
|
|
197
|
+
retire();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
ctx.effect(function* () {
|
|
201
|
+
ctx.on("agent/error", ({ agent }) => {
|
|
202
|
+
disarm(stateFor(agent));
|
|
203
|
+
});
|
|
204
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
205
|
+
stateFor(agent);
|
|
206
|
+
});
|
|
207
|
+
ctx.on("agent/disposed", ({ agent }) => {
|
|
208
|
+
states.delete(agent);
|
|
209
|
+
});
|
|
210
|
+
ctx.on("agent/session-start", ({ agent }) => {
|
|
211
|
+
const state = stateFor(agent);
|
|
212
|
+
state.attempt = void 0;
|
|
213
|
+
state.competingQueued = false;
|
|
214
|
+
state.needsCheckpoint = false;
|
|
215
|
+
});
|
|
216
|
+
ctx.on("agent/status", ({ agent, status }) => {
|
|
217
|
+
const state = stateFor(agent);
|
|
218
|
+
if (status === "idle") {
|
|
219
|
+
state.competingQueued = false;
|
|
220
|
+
const attempt = state.attempt;
|
|
221
|
+
const goal = currentGoal(state);
|
|
222
|
+
if ((attempt?.phase === "queued" || attempt?.phase === "claimed" || attempt?.cancelled) && goal?.phase === "active" && goal.activation === "armed") {
|
|
223
|
+
state.attempt = void 0;
|
|
224
|
+
try {
|
|
225
|
+
ctx.goals.pause(agent, goalRef(goal));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
ctx.logger.warn(`goal-round-driver: could not pause cancelled goal for agent "${agent.id}": ${renderThrown(error)}`);
|
|
228
|
+
disarm(state);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
requestDrive(state);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
ctx.on("goal/changed", ({ agent }) => {
|
|
235
|
+
const state = stateFor(agent);
|
|
236
|
+
state.needsCheckpoint = true;
|
|
237
|
+
requestDrive(state);
|
|
238
|
+
});
|
|
239
|
+
ctx.on("agent/inbox/inserted", ({ agent, message }) => {
|
|
240
|
+
if (!agent.inbox.nextTurn.some((candidate) => candidate.id === message.id)) return;
|
|
241
|
+
const state = stateFor(agent);
|
|
242
|
+
const attempt = state.attempt;
|
|
243
|
+
if (attempt !== void 0 && sameQueued(message.content, message.source, attempt)) return;
|
|
244
|
+
state.competingQueued = true;
|
|
245
|
+
if (attempt?.phase === "queued") attempt.stale = true;
|
|
246
|
+
});
|
|
247
|
+
ctx.on("agent/inbox/claimed", ({ agent, message }) => {
|
|
248
|
+
const attempt = stateFor(agent).attempt;
|
|
249
|
+
if (attempt !== void 0 && sameQueued(message.content, message.source, attempt)) attempt.phase = "claimed";
|
|
250
|
+
});
|
|
251
|
+
ctx.on("agent/inbox/discarded", ({ agent, message }) => {
|
|
252
|
+
const attempt = stateFor(agent).attempt;
|
|
253
|
+
if (attempt !== void 0 && sameQueued(message.content, message.source, attempt)) attempt.cancelled = true;
|
|
254
|
+
});
|
|
255
|
+
ctx.on("session/event", (session, event) => {
|
|
256
|
+
const agent = ctx.agents.get(session.id);
|
|
257
|
+
if (agent === void 0 || agent.session !== session) return;
|
|
258
|
+
const state = stateFor(agent);
|
|
259
|
+
switch (event.type) {
|
|
260
|
+
case "user/message":
|
|
261
|
+
if (state.attempt !== void 0 && event.data.id === state.attempt.messageId) state.attempt.phase = "admitted";
|
|
262
|
+
return;
|
|
263
|
+
case "turn/end":
|
|
264
|
+
if (event.data.reason.kind === "max-tokens") {
|
|
265
|
+
disarm(state);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (event.data.reason.kind !== "aborted") return;
|
|
269
|
+
if (state.attempt?.phase === "claimed" || state.attempt?.phase === "admitted") state.attempt.cancelled = true;
|
|
270
|
+
else disarm(state);
|
|
271
|
+
return;
|
|
272
|
+
default: return;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
/** Fail closed unless the queued prompt still owns the exact live revision. */
|
|
276
|
+
function validReservation(state, content, source) {
|
|
277
|
+
const attempt = state.attempt;
|
|
278
|
+
const goal = currentGoal(state);
|
|
279
|
+
return ctx.fiber.state === 2 && !state.stopping && attempt !== void 0 && attempt.phase === "claimed" && !attempt.stale && sameQueued(content, source, attempt) && goal !== void 0 && goal.id === source.goalId && goal.revision === source.revision && goal.phase === "active" && goal.activation === "armed" && source.round === goal.roundsStarted + 1;
|
|
280
|
+
}
|
|
281
|
+
ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
282
|
+
const submitted = messages.find((message) => isGoalRoundSource(message.source));
|
|
283
|
+
if (submitted === void 0) return next();
|
|
284
|
+
const { content, source } = submitted;
|
|
285
|
+
const state = stateFor(agent);
|
|
286
|
+
let valid = false;
|
|
287
|
+
try {
|
|
288
|
+
valid = validReservation(state, content, source);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
ctx.logger.warn(`goal-round-driver: pre-step check failed for agent "${agent.id}": ${renderThrown(error)}`);
|
|
291
|
+
disarm(state);
|
|
292
|
+
}
|
|
293
|
+
if (!valid) {
|
|
294
|
+
const attempt = state.attempt;
|
|
295
|
+
if (attempt !== void 0 && sameRound(source, attempt)) {
|
|
296
|
+
attempt.stale = true;
|
|
297
|
+
state.attempt = void 0;
|
|
298
|
+
}
|
|
299
|
+
restoreOtherClaimed(agent, messages, submitted.id);
|
|
300
|
+
requestDrive(state);
|
|
301
|
+
return { kind: "reject" };
|
|
302
|
+
}
|
|
303
|
+
let decision;
|
|
304
|
+
try {
|
|
305
|
+
decision = await next();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (signal.aborted) throw error;
|
|
308
|
+
state.attempt = void 0;
|
|
309
|
+
requestDrive(state);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
if (signal.aborted) {
|
|
313
|
+
if (decision.kind === "enter") restoreOtherClaimed(agent, decision.messages, submitted.id);
|
|
314
|
+
return decision;
|
|
315
|
+
}
|
|
316
|
+
if (decision.kind === "reject") {
|
|
317
|
+
state.attempt = void 0;
|
|
318
|
+
const goal = currentGoal(state);
|
|
319
|
+
if (goal !== void 0 && goal.id === source.goalId && goal.revision === source.revision && goal.phase === "active" && goal.activation === "armed") ctx.goals.block(agent, goalRef(goal), {
|
|
320
|
+
code: "prompt-rejected",
|
|
321
|
+
message: "Goal round was rejected before entering its step."
|
|
322
|
+
});
|
|
323
|
+
return decision;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
valid = validReservation(state, content, source);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
ctx.logger.warn(`goal-round-driver: post-decision check failed for agent "${agent.id}": ${renderThrown(error)}`);
|
|
329
|
+
disarm(state);
|
|
330
|
+
valid = false;
|
|
331
|
+
}
|
|
332
|
+
if (!valid) {
|
|
333
|
+
state.attempt = void 0;
|
|
334
|
+
restoreOtherClaimed(agent, decision.messages, submitted.id);
|
|
335
|
+
requestDrive(state);
|
|
336
|
+
return { kind: "reject" };
|
|
337
|
+
}
|
|
338
|
+
return decision;
|
|
339
|
+
});
|
|
340
|
+
for (const agent of ctx.agents.list()) disarm(stateFor(agent));
|
|
341
|
+
yield async () => {
|
|
342
|
+
const waits = [];
|
|
343
|
+
for (const state of states.values()) {
|
|
344
|
+
state.stopping = true;
|
|
345
|
+
disarm(state);
|
|
346
|
+
const attempt = state.attempt;
|
|
347
|
+
if (attempt !== void 0) {
|
|
348
|
+
attempt.stale = true;
|
|
349
|
+
/* v8 ignore next -- followup reserves the live agent before publishing a queued attempt */
|
|
350
|
+
if (state.agent.status === "running") {
|
|
351
|
+
state.agent.cancel({ kind: "parent" });
|
|
352
|
+
waits.push(state.agent.whenIdle());
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (state.run !== void 0) waits.push(state.run);
|
|
356
|
+
}
|
|
357
|
+
await Promise.allSettled(waits);
|
|
358
|
+
states.clear();
|
|
359
|
+
};
|
|
360
|
+
}, "goal-round-driver lifecycle");
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
export { apply, inject, name, renderGoalRoundPrompt };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { foldGoal } from "@stackstackstack/dsh-goal";
|
|
3
|
+
//#region lib/types/prompt.js
|
|
4
|
+
/** Model-visible continuation prompt for one same-session goal round. */
|
|
5
|
+
/**
|
|
6
|
+
* Render the complete goal-round instruction retained in session history.
|
|
7
|
+
* @param goal - exact active goal revision being admitted.
|
|
8
|
+
* @param round - next positive round number.
|
|
9
|
+
* @returns a fresh one-block prompt for `Agent.followup()`.
|
|
10
|
+
*/
|
|
11
|
+
function renderGoalRoundPrompt(goal, round) {
|
|
12
|
+
return [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: `<goal_round>
|
|
15
|
+
Objective: ${JSON.stringify(goal.objective)}\nRound: ${round}/${goal.maxGoalRounds}\n\nContinue working toward the objective in this same session. Treat the current workspace, tool results, and durable session state as authoritative; inspect them instead of assuming earlier narration is still current. Make concrete progress and verify the result. Before claiming completion, gather evidence that the whole objective is achieved, read the current goal, and mark it complete. If work remains, leave the goal active for the next round. Follow the configured goal-tool policy before reporting a blocker.
|
|
16
|
+
</goal_round>`
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region lib/types/invariant.js
|
|
21
|
+
/** Package-owned goal-round prompt invariants. @module @stackstackstack/dsh-goal-round-driver/invariant */
|
|
22
|
+
const PACKAGE_NAME = "@stackstackstack/dsh-goal-round-driver";
|
|
23
|
+
/** Cordis companion plugin name. */
|
|
24
|
+
const name = "goal-round-driver-invariant";
|
|
25
|
+
/** Service required before the companion can reserve package ownership. */
|
|
26
|
+
const inject = ["invariants"];
|
|
27
|
+
/** Attribute strict goal-fold failures to this companion's reconstruction. */
|
|
28
|
+
function foldChecked(events, fail) {
|
|
29
|
+
try {
|
|
30
|
+
return foldGoal(events);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return fail(`cannot reconstruct the goal before a continuation message: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Recreate the live-shaped view consumed by the package's pure prompt renderer. */
|
|
36
|
+
function goalView(folded, source, fail) {
|
|
37
|
+
const goal = folded.goal;
|
|
38
|
+
if (goal === void 0 || folded.createdAt === void 0 || folded.updatedAt === void 0 || goal.phase !== "active" || goal.id !== source.goalId || goal.revision !== source.revision || source.round !== folded.roundsStarted + 1 || source.round > goal.maxGoalRounds) return fail(`goal round ${source.round} cannot be reconstructed from the preceding durable goal state`);
|
|
39
|
+
return {
|
|
40
|
+
...goal,
|
|
41
|
+
roundsStarted: folded.roundsStarted,
|
|
42
|
+
createdAt: folded.createdAt,
|
|
43
|
+
updatedAt: folded.updatedAt,
|
|
44
|
+
activation: "armed"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Validate one package-owned continuation message against its durable prefix. */
|
|
48
|
+
function validateEvent(prior, event, fail) {
|
|
49
|
+
if (event.type !== "user/message") return;
|
|
50
|
+
const source = event.data.source;
|
|
51
|
+
if (source.kind !== "goal" || source.round <= 0) return;
|
|
52
|
+
const expected = renderGoalRoundPrompt(goalView(foldChecked(prior, fail), source, fail), source.round);
|
|
53
|
+
if (!isDeepStrictEqual(event.data.content, expected)) fail(`goal round ${source.round} content does not match the package-owned continuation prompt`);
|
|
54
|
+
}
|
|
55
|
+
/** Check existing sessions and every candidate event before Session publishes it. */
|
|
56
|
+
const install = Object.assign((ctx, fail) => {
|
|
57
|
+
for (const session of ctx.sessions.list()) {
|
|
58
|
+
const prior = [];
|
|
59
|
+
for (const event of session.events) {
|
|
60
|
+
validateEvent(prior, event, fail);
|
|
61
|
+
prior.push(event);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
65
|
+
if (eventName !== "session/event") return;
|
|
66
|
+
const [session, event] = args;
|
|
67
|
+
validateEvent(session.events, event, fail);
|
|
68
|
+
}, { global: true });
|
|
69
|
+
}, { inject: ["sessions"] });
|
|
70
|
+
/**
|
|
71
|
+
* Register the goal-round-driver invariant companion.
|
|
72
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
73
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
74
|
+
*/
|
|
75
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
76
|
+
//#endregion
|
|
77
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-session goal-round driver over public agent, session, and goal services.
|
|
3
|
+
* @module @stackstackstack/dsh-goal-round-driver
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
export { renderGoalRoundPrompt } from './prompt.ts';
|
|
7
|
+
export declare const name = "goal-round-driver";
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
/** Install automatic same-session continuation and its race fences. */
|
|
10
|
+
export declare function apply(ctx: Context): void;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned goal-round prompt invariants. @module @stackstackstack/dsh-goal-round-driver/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "goal-round-driver-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the goal-round-driver invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Model-visible continuation prompt for one same-session goal round. */
|
|
2
|
+
import type { ContentBlock } from '@stackstackstack/dsh-llm';
|
|
3
|
+
import type { GoalView } from '@stackstackstack/dsh-goal';
|
|
4
|
+
/**
|
|
5
|
+
* Render the complete goal-round instruction retained in session history.
|
|
6
|
+
* @param goal - exact active goal revision being admitted.
|
|
7
|
+
* @param round - next positive round number.
|
|
8
|
+
* @returns a fresh one-block prompt for `Agent.followup()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderGoalRoundPrompt(goal: GoalView, round: number): ContentBlock[];
|
|
11
|
+
//# sourceMappingURL=prompt.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackstackstack/dsh-goal-round-driver",
|
|
3
|
+
"description": "Race-fenced same-session goal-round driver",
|
|
4
|
+
"version": "0.1.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/goal/goal-round-driver"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@stackstackstack/dsh-agent": "^0.1.5",
|
|
36
|
+
"@stackstackstack/dsh-goal": "^0.1.5",
|
|
37
|
+
"@stackstackstack/dsh-llm": "^0.1.5",
|
|
38
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
39
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@stackstackstack/dsh-agent": "^0.1.5",
|
|
44
|
+
"@stackstackstack/dsh-agent-loop": "^0.1.5",
|
|
45
|
+
"@stackstackstack/dsh-goal": "^0.1.5",
|
|
46
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
47
|
+
"@stackstackstack/dsh-llm": "^0.1.5",
|
|
48
|
+
"@stackstackstack/dsh-agent-loop-testkit": "^0.1.5",
|
|
49
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
50
|
+
"@stackstackstack/dsh-system-prompt": "^0.1.5",
|
|
51
|
+
"@stackstackstack/dsh-tools": "^0.1.5",
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
53
|
+
}
|
|
54
|
+
}
|