@astrosheep/pi-goal-next 0.1.7 → 0.1.8
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 +1 -1
- package/docs/architecture.md +3 -3
- package/package.json +1 -1
- package/src/commands.ts +9 -2
- package/src/goal-commit.ts +1 -0
- package/src/goal.ts +10 -0
- package/src/store.ts +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ For a project-local extension, add the package path to the project's Pi extensio
|
|
|
29
29
|
/goal clear
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
With no goal, or when the current goal is complete, `/goal <objective>` creates a fresh goal. With an unfinished goal it instead updates the objective in place (Codex `thread/goal/set` semantics): same goal id, status, token budget, and cumulative usage are preserved, and an `objective_updated` steering message is sent. `/goal edit <objective>` performs the same in-place update. The model-facing `create_goal` tool still refuses while an unfinished goal exists. `pause` and `resume` are user commands; `clear` removes the goal semantically from the journal. Token suffixes accepted by create are `k` and `M` (for example, `200k` and `2M`).
|
|
33
33
|
|
|
34
34
|
## Model tools
|
|
35
35
|
|
package/docs/architecture.md
CHANGED
|
@@ -85,7 +85,7 @@ Retry messages are separately accounted because each response costs real tokens.
|
|
|
85
85
|
|
|
86
86
|
- `continuationPrompt(goal)` (`continuation.md`): the objective inside `<objective>` as user-provided data; budget block; "Work from evidence"; "Fidelity"; completion audit; blocked audit (three consecutive goal turns); closing rules.
|
|
87
87
|
- `budgetLimitPrompt(goal)` (`budget_limit.md`): sent once per goal instance when the status flips to `budget_limited`.
|
|
88
|
-
- `objectiveUpdatedPrompt(goal)` (`objective_updated.md`): sent after a successful `/goal edit
|
|
88
|
+
- `objectiveUpdatedPrompt(goal)` (`objective_updated.md`): sent after a successful in-place objective update (`/goal <new>` or `/goal edit` on an unfinished goal); uses `<untrusted_objective>` and reports remaining tokens as `unknown` when no budget is set.
|
|
89
89
|
|
|
90
90
|
Substitution is trivial `{{ name }}` replacement. `escapeXmlText` (`&`→`&`, `<`→`<`, `>`→`>`) is applied to the objective only. `tokens_used` is `input+output+cacheRead+cacheWrite`; `token_budget` is the budget or `none`; `remaining_tokens` is `max(0, budget-used)`, or `unbounded` (continuation) / `unknown` (objective update) without a budget; `time_used_seconds` is `floor((Date.now()-createdAt)/1000)`.
|
|
91
91
|
|
|
@@ -93,9 +93,9 @@ The blocked audit is prompt-level only: the runtime does not count blocking turn
|
|
|
93
93
|
|
|
94
94
|
## Commands
|
|
95
95
|
|
|
96
|
-
`/goal` or `/goal status` · `/goal [--tokens N[k|M]] <objective>` (create
|
|
96
|
+
`/goal` or `/goal status` · `/goal [--tokens N[k|M]] <objective>` (create, or in-place objective update while unfinished — Codex `thread/goal/set` semantics) · `/goal edit <objective>` (same in-place update) · `/goal pause` · `/goal resume` · `/goal clear` · `/goal budget <tokens|none>` · `/goal turns <max-continuations>`.
|
|
97
97
|
|
|
98
|
-
A successful `resume` atomically journals `resetContinuations: true` on the user transition to active, resetting the run's continuation count while preserving the objective, usage, budget, and historical entries. It also accepts an already-active goal. Exhausted token budgets and zero continuation allowances are reported without resuming. A successful `create` or `resume` calls `continuation.onSettled()` so an idle session starts pursuing immediately (Codex starts the turn directly). A successful
|
|
98
|
+
A successful `resume` atomically journals `resetContinuations: true` on the user transition to active, resetting the run's continuation count while preserving the objective, usage, budget, and historical entries. It also accepts an already-active goal. Exhausted token budgets and zero continuation allowances are reported without resuming. A successful `create` or `resume` calls `continuation.onSettled()` so an idle session starts pursuing immediately (Codex starts the turn directly). A successful in-place objective update atomically appends `goal.objective_updated`, preserving the goal id, status, limits, and cumulative usage, then sends `objectiveUpdatedPrompt(goal)` with `triggerTurn: true`; `pause` and `clear` send nothing (the active-status check stops continuation).
|
|
99
99
|
|
|
100
100
|
## Defaults
|
|
101
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/pi-goal-next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Persistent autonomous goals for pi, with Codex-verbatim goal semantics: continuation prompts, self-audited completion, budgets, and CAS-journaled state.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/src/commands.ts
CHANGED
|
@@ -33,14 +33,21 @@ export function registerGoalCommands(piLike: CommandPiLike, deps: CommandDeps):
|
|
|
33
33
|
switch (cmd.kind) {
|
|
34
34
|
case "status": return summarize(current);
|
|
35
35
|
case "create": {
|
|
36
|
-
|
|
36
|
+
// Codex thread/goal/set semantics: an unfinished goal's objective is
|
|
37
|
+
// updated in place (same goal id, usage/budget preserved); a missing
|
|
38
|
+
// or complete goal starts a fresh one.
|
|
39
|
+
if (current && current.goal.status !== "complete") {
|
|
40
|
+
const r = await goalCommit.commit({ type: "update_objective", objective: cmd.objective as string, ...(cmd.tokenBudget === undefined || cmd.tokenBudget === null ? {} : { tokenBudget: cmd.tokenBudget }) }, revision);
|
|
41
|
+
if (r.kind === "ok" && r.snapshot) deps.send({ customType: "pi-goal-next/objective_updated", content: objectiveUpdatedPrompt(r.snapshot.goal), display: false, details: { goalId: r.snapshot.goal.id } }, { triggerTurn: true });
|
|
42
|
+
return r.kind === "ok" ? "Goal updated." : "Goal update failed.";
|
|
43
|
+
}
|
|
37
44
|
const r = await goalCommit.commit({ type: "create", id: newGoalId(), objective: cmd.objective as string, tokenBudget: cmd.tokenBudget ?? null }, revision);
|
|
38
45
|
if (r.kind === "ok") await deps.kick();
|
|
39
46
|
return r.kind === "ok" ? "Goal created." : "Goal update failed.";
|
|
40
47
|
}
|
|
41
48
|
case "edit": {
|
|
42
49
|
if (!current) return "Goal update failed.";
|
|
43
|
-
const r = await goalCommit.commit({ type: "
|
|
50
|
+
const r = await goalCommit.commit({ type: "update_objective", objective: cmd.objective as string }, revision);
|
|
44
51
|
if (r.kind === "ok" && r.snapshot) deps.send({ customType: "pi-goal-next/objective_updated", content: objectiveUpdatedPrompt(r.snapshot.goal), display: false, details: { goalId: r.snapshot.goal.id } }, { triggerTurn: true });
|
|
45
52
|
return r.kind === "ok" ? "Goal edited." : "Goal update failed.";
|
|
46
53
|
}
|
package/src/goal-commit.ts
CHANGED
|
@@ -41,6 +41,7 @@ export function createGoalCommit(store: GoalStore) {
|
|
|
41
41
|
switch (intent.type) {
|
|
42
42
|
case "create": return { type: "goal.created", version: 1, seq, goal: next! };
|
|
43
43
|
case "replace": return { type: "goal.replaced", version: 1, seq, goal: next! };
|
|
44
|
+
case "update_objective": return { type: "goal.objective_updated", version: 1, seq, objective: intent.objective };
|
|
44
45
|
case "clear": return { type: "goal.cleared", version: 1, seq };
|
|
45
46
|
case "transition": return { type: "goal.transition", version: 1, seq, from: previous!.status, to: intent.to, by: intent.by, ...(intent.userRequest ? { userRequest: intent.userRequest } : {}), ...(intent.resetContinuations ? { resetContinuations: true } : {}) };
|
|
46
47
|
case "usage": return { type: "goal.usage", version: 1, seq, input: intent.input === undefined ? 0 : intent.input, output: intent.output === undefined ? 0 : intent.output, cacheRead: intent.cacheRead === undefined ? 0 : intent.cacheRead, cacheWrite: intent.cacheWrite === undefined ? 0 : intent.cacheWrite, unknownMessages: intent.unknownMessages ?? 0 };
|
package/src/goal.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type Goal = {
|
|
|
8
8
|
export type Entry =
|
|
9
9
|
| { type: "goal.created"; version: 1; seq: number; goal: Goal }
|
|
10
10
|
| { type: "goal.replaced"; version: 1; seq: number; goal: Goal }
|
|
11
|
+
| { type: "goal.objective_updated"; version: 1; seq: number; objective: string }
|
|
11
12
|
| { type: "goal.transition"; version: 1; seq: number; from: Status; to: Status; by: Actor; userRequest?: string; resetContinuations?: boolean }
|
|
12
13
|
| { type: "goal.cleared"; version: 1; seq: number }
|
|
13
14
|
| { type: "goal.usage"; version: 1; seq: number; input: number | null; output: number | null; cacheRead: number | null; cacheWrite: number | null; unknownMessages: number }
|
|
@@ -17,6 +18,7 @@ export type Entry =
|
|
|
17
18
|
export type Intent =
|
|
18
19
|
| { type: "create"; id: string; objective: string; tokenBudget?: number | null; maxContinuations?: number }
|
|
19
20
|
| { type: "replace"; id: string; objective: string }
|
|
21
|
+
| { type: "update_objective"; objective: string; tokenBudget?: number | null }
|
|
20
22
|
| { type: "transition"; to: Status; by: Actor; userRequest?: string; resetContinuations?: boolean }
|
|
21
23
|
| { type: "clear" }
|
|
22
24
|
| { type: "usage"; input?: number | null; output?: number | null; cacheRead?: number | null; cacheWrite?: number | null; unknownMessages?: number }
|
|
@@ -56,6 +58,13 @@ export function transition(state: Goal | null, intent: Intent): Goal | null {
|
|
|
56
58
|
}
|
|
57
59
|
if (!state) throw new GoalError("missing", "no goal exists");
|
|
58
60
|
if (intent.type === "clear") return null;
|
|
61
|
+
if (intent.type === "update_objective") {
|
|
62
|
+
if (typeof intent.objective !== "string" || !intent.objective.trim()) throw new GoalError("invalid", "objective is required");
|
|
63
|
+
if (intent.tokenBudget !== undefined && intent.tokenBudget !== null && (!Number.isFinite(intent.tokenBudget) || intent.tokenBudget < 0)) throw new GoalError("invalid", "invalid limits");
|
|
64
|
+
// Codex thread/goal/set semantics: update the objective in place, preserving
|
|
65
|
+
// status, limits, and cumulative usage on the same goal id.
|
|
66
|
+
return { ...state, objective: intent.objective.trim(), ...(intent.tokenBudget !== undefined ? { tokenBudget: intent.tokenBudget } : {}) };
|
|
67
|
+
}
|
|
59
68
|
if (intent.type === "transition") {
|
|
60
69
|
if (!statuses.includes(intent.to)) throw new GoalError("invalid", "unknown status");
|
|
61
70
|
if (intent.to === "paused" && !intent.userRequest?.trim()) throw new GoalError("forbidden", "paused requires user request evidence");
|
|
@@ -104,6 +113,7 @@ export function fold(entries: readonly Entry[]): Goal | null {
|
|
|
104
113
|
if (!entry.type.startsWith("goal.")) continue;
|
|
105
114
|
if (entry.type === "goal.created" || entry.type === "goal.replaced") state = { ...entry.goal };
|
|
106
115
|
else if (entry.type === "goal.cleared") state = null;
|
|
116
|
+
else if (state && entry.type === "goal.objective_updated") state = transition(state, { type: "update_objective", objective: entry.objective });
|
|
107
117
|
else if (state && entry.type === "goal.transition") state = transition(state, { type: "transition", to: entry.to, by: entry.by, userRequest: entry.userRequest, resetContinuations: entry.resetContinuations });
|
|
108
118
|
else if (state && entry.type === "goal.limit_config") state = transition(state, { type: "limit_config", tokenBudget: entry.tokenBudget, maxContinuations: entry.maxContinuations });
|
|
109
119
|
else if (state && entry.type === "goal.continuation_sent") state = transition(state, { type: "continuation_sent", generation: entry.generation });
|
package/src/store.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type BranchContext = { sessionManager: { getBranch(): readonly unknown[]
|
|
|
3
3
|
export type PiCustomEntry = { type: "custom"; customType: string; data: unknown };
|
|
4
4
|
export type PiAppender = { appendEntry(customType: string, data: unknown): unknown };
|
|
5
5
|
export const VERSION = 1 as const;
|
|
6
|
-
const entryTypes = new Set(["goal.created", "goal.replaced", "goal.transition", "goal.cleared", "goal.usage", "goal.continuation_sent", "goal.stale_turn", "goal.limit_config"]);
|
|
6
|
+
const entryTypes = new Set(["goal.created", "goal.replaced", "goal.objective_updated", "goal.transition", "goal.cleared", "goal.usage", "goal.continuation_sent", "goal.stale_turn", "goal.limit_config"]);
|
|
7
7
|
export function isEntry(entry: unknown): entry is Entry { return !!entry && typeof entry === "object" && (entry as { version?: unknown }).version === VERSION && entryTypes.has((entry as { type?: unknown }).type as string) && Number.isSafeInteger((entry as { seq?: unknown }).seq); }
|
|
8
8
|
export function readBranch(ctx: BranchContext): Entry[] { return ctx.sessionManager.getBranch().filter((item): item is PiCustomEntry => { if (!item || typeof item !== "object") return false; const wrapped = item as Partial<PiCustomEntry>; return wrapped.type === "custom" && wrapped.customType === "pi-goal-next"; }).map(item => item.data).filter(isEntry); }
|
|
9
9
|
export async function append(pi: PiAppender, entry: Entry): Promise<void> { if (!isEntry(entry)) throw new Error("unsupported journal version"); await pi.appendEntry("pi-goal-next", entry); }
|