@pasko70/pibo 1.9.11 → 1.10.0
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/dist/apps/chat/agent-profiles.js +2 -2
- package/dist/apps/chat/agent-store.js +16 -3
- package/dist/apps/chat/chat-request-normalizers.js +10 -0
- package/dist/apps/chat/data/timeline-query-service.js +11 -0
- package/dist/apps/chat/loop-api.js +176 -0
- package/dist/apps/chat/trace.js +2 -0
- package/dist/apps/chat/web-app.js +13 -6
- package/dist/apps/chat/workflow-manual-trigger-runtime.js +149 -46
- package/dist/apps/chat-ui/assets/{dist-yCYNNb5d.js → dist-BwKObYnX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BCB6zezO.js → dist-CKtT8YGm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CnVsqwSG.js → dist-CS7wdk0Z.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-HqTN67dc.js → dist-D-cxLQO1.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BNMu92bb.js → dist-DUlaXAk7.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CzE6k3F3.js → dist-DlATLa-U.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dq4GxJi3.js → dist-GdEM8UW1.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BZ2eTC4f.js → dist-LHRs1Nhr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D811wJeV.js → dist-Y-AA2omI.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-WyXdYl-w.js → dist-nOLTkZrJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BHa-kcGl.js → dist-wE9nop9V.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-DwHJfmiF.js → index-8W_yMHQI.js} +6 -6
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/cli.js +23 -6
- package/dist/core/routed-session.js +30 -1
- package/dist/core/runtime.js +6 -1
- package/dist/core/session-router.js +4 -2
- package/dist/data/ingest-service.js +7 -0
- package/dist/data/message-store.js +11 -0
- package/dist/data/schema.js +2 -0
- package/dist/gateway/server.js +4 -2
- package/dist/gateway/web.js +2 -2
- package/dist/loops/channel.js +8 -0
- package/dist/loops/cli.js +208 -0
- package/dist/loops/plugin.js +16 -0
- package/dist/loops/prompts.js +83 -0
- package/dist/loops/service.js +357 -0
- package/dist/loops/stopping.js +170 -0
- package/dist/loops/store.js +531 -0
- package/dist/loops/templates.js +232 -0
- package/dist/loops/tools.js +167 -0
- package/dist/loops/types.js +1 -0
- package/dist/plugins/builtin.js +8 -0
- package/dist/plugins/registry.js +23 -12
- package/dist/resources/lifecycle.js +4 -6
- package/dist/resources/reaper-state.js +30 -3
- package/dist/resources/reaper.js +43 -15
- package/dist/shared/trace-engine.js +3 -2
- package/dist/shared/trace-event-projection.js +26 -0
- package/dist/tools/guides.js +51 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/registry.js +21 -3
- package/package.json +5 -2
- package/skills/builtin/loop/SKILL.md +69 -0
- package/skills/builtin/ralph-loop/SKILL.md +4 -2
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const promiseCompletePolicy = {
|
|
2
|
+
mode: 'any',
|
|
3
|
+
conditions: [{ id: 'promise-complete', type: 'pibo.loop.promise-complete' }],
|
|
4
|
+
};
|
|
5
|
+
const goalStatusPolicy = {
|
|
6
|
+
mode: 'any',
|
|
7
|
+
conditions: [{ id: 'goal-status', type: 'pibo.loop.goal-status' }],
|
|
8
|
+
};
|
|
9
|
+
const singleRunPolicy = {
|
|
10
|
+
mode: 'any',
|
|
11
|
+
conditions: [{ id: 'max-iterations', type: 'pibo.loop.max-iterations' }],
|
|
12
|
+
};
|
|
13
|
+
function markdown(strings) {
|
|
14
|
+
return String.raw(strings).replaceAll('\\`', '`');
|
|
15
|
+
}
|
|
16
|
+
export const BUILT_IN_LOOP_JOB_TEMPLATES = [
|
|
17
|
+
{
|
|
18
|
+
id: 'goal-objective',
|
|
19
|
+
name: 'Goal objective',
|
|
20
|
+
description: 'Default same-session goal loop: keep pursuing the full objective across turns until completion is proven or another stop condition is satisfied.',
|
|
21
|
+
category: 'general',
|
|
22
|
+
job: {
|
|
23
|
+
mode: 'goal',
|
|
24
|
+
name: 'Goal loop',
|
|
25
|
+
description: 'Long-running objective continued in one Pibo Session.',
|
|
26
|
+
prompt: markdown `# Objective
|
|
27
|
+
|
|
28
|
+
<Describe the complete requested end state.>
|
|
29
|
+
|
|
30
|
+
# Success criteria
|
|
31
|
+
|
|
32
|
+
- <Observable completion criterion>
|
|
33
|
+
- <Required verification>
|
|
34
|
+
- <Behavior that must not regress>
|
|
35
|
+
|
|
36
|
+
# Constraints
|
|
37
|
+
|
|
38
|
+
- <Scope, safety, or approval boundary>`,
|
|
39
|
+
stopPolicy: goalStatusPolicy,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'prd-single-story-standard',
|
|
44
|
+
name: 'PRD single story standard',
|
|
45
|
+
description: 'Legacy Ralph loop: pick the highest-priority failing PRD user story, implement exactly one story, test, commit, update progress, then stop or continue on the next run.',
|
|
46
|
+
category: 'prd',
|
|
47
|
+
job: {
|
|
48
|
+
mode: 'ralph',
|
|
49
|
+
name: 'Ralph PRD single story',
|
|
50
|
+
description: 'One PRD user story per Loop iteration.',
|
|
51
|
+
prompt: markdown `# Ralph Auftrag: <Project / Change Name>
|
|
52
|
+
|
|
53
|
+
## Worktree
|
|
54
|
+
|
|
55
|
+
Work from the host worktree, not from a container copy:
|
|
56
|
+
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
cd <host-worktree>
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Branch: \`<branch-name>\`
|
|
62
|
+
Base: \`<base-branch-or-commit>\`
|
|
63
|
+
PRDs: \`<path-to-prds>/prd_*.json\`
|
|
64
|
+
Progress log: \`<path-to-prds>/progress.txt\`
|
|
65
|
+
|
|
66
|
+
If a container is required for build, gateway, or browser checks, use it only for running commands. Edit files and run git commands in the host worktree. Do not edit \`/app\` inside a container.
|
|
67
|
+
|
|
68
|
+
## Ralph Agent Instructions
|
|
69
|
+
|
|
70
|
+
You are an autonomous coding agent working on a software project.
|
|
71
|
+
|
|
72
|
+
### Your Task
|
|
73
|
+
|
|
74
|
+
1. Work from the host worktree above.
|
|
75
|
+
2. Read all PRD JSON files matching \`<path-to-prds>/prd_*.json\`.
|
|
76
|
+
3. Read the progress log. If it does not exist, create it.
|
|
77
|
+
4. Read the \`## Codebase Patterns\` section at the top of the progress log if present.
|
|
78
|
+
5. Pick the highest-priority user story where \`passes: false\`.
|
|
79
|
+
6. Implement that single user story only.
|
|
80
|
+
7. Run quality checks:
|
|
81
|
+
- Always run \`npm run typecheck\`.
|
|
82
|
+
- Run relevant tests for touched areas.
|
|
83
|
+
- For UI stories, run browser verification in the configured dev container/browser environment.
|
|
84
|
+
8. If checks pass, update the PRD JSON to set \`passes: true\` for the completed story.
|
|
85
|
+
9. Commit all changes from the host worktree with message:
|
|
86
|
+
\`feat: [Story ID] - [Story Title]\`
|
|
87
|
+
10. Append your progress to the progress log.
|
|
88
|
+
|
|
89
|
+
### Progress Report Format
|
|
90
|
+
|
|
91
|
+
Append to the progress log. Never replace; always append.
|
|
92
|
+
|
|
93
|
+
\`\`\`md
|
|
94
|
+
## [Date/Time] - [Story ID]
|
|
95
|
+
- What was implemented
|
|
96
|
+
- Files changed
|
|
97
|
+
- Quality checks run
|
|
98
|
+
- Browser verification, if UI changed
|
|
99
|
+
- **Learnings for future iterations:**
|
|
100
|
+
- Patterns discovered
|
|
101
|
+
- Gotchas encountered
|
|
102
|
+
- Useful context
|
|
103
|
+
---
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
### Consolidate Patterns
|
|
107
|
+
|
|
108
|
+
If you discover a reusable pattern, add it to the \`## Codebase Patterns\` section at the top of the progress log. Only add general reusable patterns, not story-specific notes.
|
|
109
|
+
|
|
110
|
+
## Quality Requirements
|
|
111
|
+
|
|
112
|
+
- All commits must pass quality checks.
|
|
113
|
+
- Do not commit broken code.
|
|
114
|
+
- Keep changes focused and minimal.
|
|
115
|
+
- Follow existing code patterns.
|
|
116
|
+
- For UI stories, browser verification is mandatory.
|
|
117
|
+
|
|
118
|
+
## Stop Condition
|
|
119
|
+
|
|
120
|
+
After completing one user story, check whether all stories in all PRD JSON files have \`passes: true\`.
|
|
121
|
+
|
|
122
|
+
If all stories are complete and passing, reply with the XML completion marker on its own line. Compose it from the opening tag \`<promise>\`, the word \`COMPLETE\`, and the closing tag \`</promise>\`.
|
|
123
|
+
|
|
124
|
+
Do not quote, negate, explain, or mention that literal marker unless all stories are complete and you intend to stop the job. If any story remains with \`passes: false\`, end normally so another iteration can pick up the next story and say only that the completion marker was omitted.`,
|
|
125
|
+
stopPolicy: promiseCompletePolicy,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'prd-batch-stories',
|
|
130
|
+
name: 'PRD batch stories',
|
|
131
|
+
description: 'Batch loop: implement several failing PRD user stories in priority order in one Loop run, committing after each completed story.',
|
|
132
|
+
category: 'prd',
|
|
133
|
+
job: {
|
|
134
|
+
mode: 'ralph',
|
|
135
|
+
name: 'Ralph PRD batch',
|
|
136
|
+
description: 'Multiple PRD user stories per Loop run.',
|
|
137
|
+
prompt: markdown `# Ralph Auftrag: <Project / Change Name> Batch
|
|
138
|
+
|
|
139
|
+
## Worktree
|
|
140
|
+
|
|
141
|
+
Work from the host worktree:
|
|
142
|
+
|
|
143
|
+
\`\`\`bash
|
|
144
|
+
cd <host-worktree>
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
PRDs: \`<path-to-prds>/prd_*.json\`
|
|
148
|
+
Progress log: \`<path-to-prds>/progress.txt\`
|
|
149
|
+
Batch limit: \`<max-stories-this-run>\`
|
|
150
|
+
|
|
151
|
+
## Task
|
|
152
|
+
|
|
153
|
+
1. Read the PRD JSON files and progress log.
|
|
154
|
+
2. Work through failing user stories in priority order.
|
|
155
|
+
3. Complete up to \`<max-stories-this-run>\` stories in this run.
|
|
156
|
+
4. Keep each story isolated:
|
|
157
|
+
- implement one story;
|
|
158
|
+
- run relevant checks;
|
|
159
|
+
- set that story's \`passes\` to \`true\` only after checks pass;
|
|
160
|
+
- commit with \`feat: [Story ID] - [Story Title]\`;
|
|
161
|
+
- append a progress-log entry.
|
|
162
|
+
5. Stop early if a story is blocked, ambiguous, or tests fail. Do not continue with later stories after a failed story.
|
|
163
|
+
|
|
164
|
+
## Quality Requirements
|
|
165
|
+
|
|
166
|
+
- Always run \`npm run typecheck\` before each story commit unless the repository has a documented narrower check for this batch.
|
|
167
|
+
- Run relevant tests for touched areas.
|
|
168
|
+
- Browser verification is mandatory for UI changes.
|
|
169
|
+
- Keep changes focused; do not bundle unrelated refactors.
|
|
170
|
+
|
|
171
|
+
## Stop Condition
|
|
172
|
+
|
|
173
|
+
If all stories in all PRD JSON files are complete and passing, reply with the XML completion marker on its own line. Compose it from the opening tag \`<promise>\`, the word \`COMPLETE\`, and the closing tag \`</promise>\`.
|
|
174
|
+
|
|
175
|
+
Do not quote, negate, explain, or mention that literal marker unless all stories are complete and you intend to stop the job. Otherwise report the stories completed and the next remaining failing story, and say only that the completion marker was omitted.`,
|
|
176
|
+
stopPolicy: promiseCompletePolicy,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'single-run-objective',
|
|
181
|
+
name: 'Single-run objective',
|
|
182
|
+
description: 'Non-PRD template for one focused objective with explicit done criteria. Stops after one completed run attempt by max-iterations.',
|
|
183
|
+
category: 'general',
|
|
184
|
+
job: {
|
|
185
|
+
mode: 'ralph',
|
|
186
|
+
name: 'Ralph single-run objective',
|
|
187
|
+
description: 'One focused non-PRD objective.',
|
|
188
|
+
maxIterations: 1,
|
|
189
|
+
stopPolicy: singleRunPolicy,
|
|
190
|
+
prompt: markdown `# Ralph Auftrag: <Objective>
|
|
191
|
+
|
|
192
|
+
## Worktree
|
|
193
|
+
|
|
194
|
+
\`\`\`bash
|
|
195
|
+
cd <host-worktree>
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
Branch: \`<branch-name>\`
|
|
199
|
+
|
|
200
|
+
## Objective
|
|
201
|
+
|
|
202
|
+
<Describe the concrete outcome.>
|
|
203
|
+
|
|
204
|
+
## Done Criteria
|
|
205
|
+
|
|
206
|
+
- <Pass/fail criterion 1>
|
|
207
|
+
- <Pass/fail criterion 2>
|
|
208
|
+
- <Required verification command or manual check>
|
|
209
|
+
|
|
210
|
+
## Instructions
|
|
211
|
+
|
|
212
|
+
1. Inspect the relevant code and docs before editing.
|
|
213
|
+
2. State assumptions in code comments or docs only when they matter for maintainers.
|
|
214
|
+
3. Implement the smallest focused change that satisfies the done criteria.
|
|
215
|
+
4. Run \`npm run typecheck\` and relevant tests.
|
|
216
|
+
5. Commit with a concise message if checks pass.
|
|
217
|
+
6. End with a short report listing files changed and verification run.
|
|
218
|
+
|
|
219
|
+
This is not a PRD loop. Do not edit PRD JSON progress unless the objective explicitly asks for it.`,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
export function listLoopJobTemplates() {
|
|
224
|
+
return BUILT_IN_LOOP_JOB_TEMPLATES.map((template) => cloneTemplate(template));
|
|
225
|
+
}
|
|
226
|
+
export function getLoopJobTemplate(id) {
|
|
227
|
+
const template = BUILT_IN_LOOP_JOB_TEMPLATES.find((item) => item.id === id);
|
|
228
|
+
return template ? cloneTemplate(template) : undefined;
|
|
229
|
+
}
|
|
230
|
+
function cloneTemplate(template) {
|
|
231
|
+
return JSON.parse(JSON.stringify(template));
|
|
232
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { StringEnum, Type } from '@earendil-works/pi-ai';
|
|
2
|
+
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { createDefaultPiboLoopStore } from './store.js';
|
|
4
|
+
export const PIBO_GOAL_TOOL_NAMES = ['get_goal', 'create_goal', 'update_goal'];
|
|
5
|
+
let configuredStorePath;
|
|
6
|
+
export function configurePiboGoalToolStorePath(path) {
|
|
7
|
+
configuredStorePath = path;
|
|
8
|
+
}
|
|
9
|
+
function toolResult(value, isError = false) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
12
|
+
details: value,
|
|
13
|
+
...(isError ? { isError: true } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function errorResult(error) {
|
|
17
|
+
return toolResult({ ok: false, error: error instanceof Error ? error.message : String(error) }, true);
|
|
18
|
+
}
|
|
19
|
+
function requireSessionContext(context) {
|
|
20
|
+
const piboSessionId = context.piboSessionId?.trim();
|
|
21
|
+
if (!piboSessionId)
|
|
22
|
+
throw new Error('Goal tools require the current Pibo Session ID');
|
|
23
|
+
return {
|
|
24
|
+
piboSessionId,
|
|
25
|
+
piboRoomId: context.piboRoomId?.trim() || undefined,
|
|
26
|
+
profileName: context.profileName?.trim() || 'base',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function positiveInteger(value, field) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!Number.isInteger(value) || value < 1)
|
|
33
|
+
throw new Error(`${field} must be a positive integer`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function goalPayload(job) {
|
|
37
|
+
const tokensUsed = job.state.tokensUsed ?? 0;
|
|
38
|
+
const tokenBudget = job.tokenBudget;
|
|
39
|
+
return {
|
|
40
|
+
goalId: job.id,
|
|
41
|
+
objective: job.prompt,
|
|
42
|
+
status: effectiveGoalStatus(job),
|
|
43
|
+
tokenBudget: tokenBudget ?? null,
|
|
44
|
+
tokensUsed,
|
|
45
|
+
remainingTokens: tokenBudget === undefined ? null : Math.max(0, tokenBudget - tokensUsed),
|
|
46
|
+
timeUsedSeconds: job.state.timeUsedSeconds ?? 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function effectiveGoalStatus(job) {
|
|
50
|
+
return job.state.goalStatus ?? (job.enabled ? 'active' : 'paused');
|
|
51
|
+
}
|
|
52
|
+
async function withStore(options, action) {
|
|
53
|
+
if (options.store)
|
|
54
|
+
return await action(options.store);
|
|
55
|
+
const store = createDefaultPiboLoopStore({ path: configuredStorePath });
|
|
56
|
+
try {
|
|
57
|
+
return await action(store);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
store.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createGetGoalTool(context, options) {
|
|
64
|
+
return defineTool({
|
|
65
|
+
name: 'get_goal',
|
|
66
|
+
label: 'Get Goal',
|
|
67
|
+
description: 'Get the current goal for this Pibo Session, including status, token budget, consumed tokens, remaining tokens, and elapsed active time.',
|
|
68
|
+
promptSnippet: 'Use get_goal when you need the authoritative persisted status or accounting for the current Pibo Session goal.',
|
|
69
|
+
parameters: Type.Object({}),
|
|
70
|
+
async execute() {
|
|
71
|
+
try {
|
|
72
|
+
const { piboSessionId } = requireSessionContext(context);
|
|
73
|
+
return await withStore(options, (store) => {
|
|
74
|
+
const job = store.getLatestGoalForSession(piboSessionId);
|
|
75
|
+
return toolResult({ ok: true, goal: job ? goalPayload(job) : null });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return errorResult(error);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function createCreateGoalTool(context, options) {
|
|
85
|
+
return defineTool({
|
|
86
|
+
name: 'create_goal',
|
|
87
|
+
label: 'Create Goal',
|
|
88
|
+
description: 'Create an active persisted Goal Loop for this Pibo Session only when explicitly requested. Fails while this session already has an unfinished goal.',
|
|
89
|
+
promptSnippet: 'Call create_goal only when the user or system explicitly requests a persistent goal. Do not infer a goal from an ordinary task.',
|
|
90
|
+
parameters: Type.Object({
|
|
91
|
+
objective: Type.String({ description: 'Concrete objective to pursue across automatic continuations.' }),
|
|
92
|
+
token_budget: Type.Optional(Type.Number({ description: 'Optional positive token budget. Omit unless explicitly requested.' })),
|
|
93
|
+
}),
|
|
94
|
+
async execute(_toolCallId, params) {
|
|
95
|
+
try {
|
|
96
|
+
const session = requireSessionContext(context);
|
|
97
|
+
const objective = params.objective?.trim();
|
|
98
|
+
if (!objective)
|
|
99
|
+
throw new Error('objective is required');
|
|
100
|
+
const tokenBudget = positiveInteger(params.token_budget, 'token_budget');
|
|
101
|
+
return await withStore(options, (store) => {
|
|
102
|
+
const existing = store.getLatestGoalForSession(session.piboSessionId);
|
|
103
|
+
if (existing && effectiveGoalStatus(existing) !== 'complete') {
|
|
104
|
+
throw new Error('cannot create a new goal because this Pibo Session has an unfinished goal; complete the existing goal first');
|
|
105
|
+
}
|
|
106
|
+
const job = store.createJob({
|
|
107
|
+
mode: 'goal',
|
|
108
|
+
enabled: true,
|
|
109
|
+
target: session.piboRoomId ? { kind: 'room', roomId: session.piboRoomId } : { kind: 'default-chat' },
|
|
110
|
+
profile: session.profileName,
|
|
111
|
+
prompt: objective,
|
|
112
|
+
tokenBudget,
|
|
113
|
+
initialPiboSessionId: session.piboSessionId,
|
|
114
|
+
});
|
|
115
|
+
return toolResult({ ok: true, goal: goalPayload(job) });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return errorResult(error);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function createUpdateGoalTool(context, options) {
|
|
125
|
+
return defineTool({
|
|
126
|
+
name: 'update_goal',
|
|
127
|
+
label: 'Update Goal',
|
|
128
|
+
description: 'Mark the current goal complete or genuinely blocked. Complete requires verified achievement. Blocked requires the same impasse for at least three consecutive goal turns.',
|
|
129
|
+
promptSnippet: 'Use update_goal only with status complete after a requirement-by-requirement completion audit, or blocked after the strict repeated-blocker audit.',
|
|
130
|
+
parameters: Type.Object({
|
|
131
|
+
status: StringEnum(['complete', 'blocked'], { description: 'Terminal status for the current goal.' }),
|
|
132
|
+
}),
|
|
133
|
+
async execute(_toolCallId, params) {
|
|
134
|
+
try {
|
|
135
|
+
const { piboSessionId } = requireSessionContext(context);
|
|
136
|
+
if (params.status !== 'complete' && params.status !== 'blocked')
|
|
137
|
+
throw new Error('status must be complete or blocked');
|
|
138
|
+
const status = params.status;
|
|
139
|
+
return await withStore(options, (store) => {
|
|
140
|
+
const existing = store.getLatestGoalForSession(piboSessionId);
|
|
141
|
+
if (!existing)
|
|
142
|
+
throw new Error('cannot update goal because this Pibo Session has no goal');
|
|
143
|
+
const job = store.updateGoalStatus(existing.id, status);
|
|
144
|
+
if (!job)
|
|
145
|
+
throw new Error('goal no longer exists');
|
|
146
|
+
return toolResult({
|
|
147
|
+
ok: true,
|
|
148
|
+
goal: goalPayload(job),
|
|
149
|
+
...(status === 'complete' && job.tokenBudget !== undefined
|
|
150
|
+
? { completionBudgetReport: `${job.state.tokensUsed ?? 0}/${job.tokenBudget} reported tokens consumed before the current model turn finishes` }
|
|
151
|
+
: {}),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return errorResult(error);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export function createPiboGoalToolDefinitions(context, options = {}) {
|
|
162
|
+
return [
|
|
163
|
+
createGetGoalTool(context, options),
|
|
164
|
+
createCreateGoalTool(context, options),
|
|
165
|
+
createUpdateGoalTool(context, options),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/plugins/builtin.js
CHANGED
|
@@ -112,6 +112,7 @@ function requireLogoutParams(event) {
|
|
|
112
112
|
}
|
|
113
113
|
function createBaseProfileBuilder(profileName, context) {
|
|
114
114
|
return addPiboNativeToolingContext(new InitialSessionContextBuilder(profileName)
|
|
115
|
+
.withToolPackages({ goalControl: true })
|
|
115
116
|
.addSkill(context.getSkill("pi-agent-harness")), context);
|
|
116
117
|
}
|
|
117
118
|
export const piboCorePlugin = definePiboPlugin({
|
|
@@ -148,6 +149,11 @@ export const piboCorePlugin = definePiboPlugin({
|
|
|
148
149
|
path: builtinSkillPath("skill-creator"),
|
|
149
150
|
kind: "builtin",
|
|
150
151
|
});
|
|
152
|
+
api.registerSkill({
|
|
153
|
+
name: "loop",
|
|
154
|
+
path: builtinSkillPath("loop"),
|
|
155
|
+
kind: "builtin",
|
|
156
|
+
});
|
|
151
157
|
api.registerSkill({
|
|
152
158
|
name: "ralph-loop",
|
|
153
159
|
path: builtinSkillPath("ralph-loop"),
|
|
@@ -167,6 +173,7 @@ export const piboCorePlugin = definePiboPlugin({
|
|
|
167
173
|
create() {
|
|
168
174
|
return new InitialSessionContextBuilder(DEFAULT_PIBO_PROFILE_NAME)
|
|
169
175
|
.withBuiltinToolNames(["read", "bash", "edit", "write"])
|
|
176
|
+
.withToolPackages({ goalControl: true })
|
|
170
177
|
.createSession();
|
|
171
178
|
},
|
|
172
179
|
});
|
|
@@ -451,6 +458,7 @@ export function selectDefaultPiboProfileName(registry) {
|
|
|
451
458
|
export function createDefaultPiboProfile() {
|
|
452
459
|
return new InitialSessionContextBuilder(DEFAULT_PIBO_PROFILE_NAME)
|
|
453
460
|
.withBuiltinToolNames(["read", "bash", "edit", "write"])
|
|
461
|
+
.withToolPackages({ goalControl: true })
|
|
454
462
|
.createSession();
|
|
455
463
|
}
|
|
456
464
|
export function resolvePiboProfileNameFromRegistryOrDefault(registry, profileName) {
|
package/dist/plugins/registry.js
CHANGED
|
@@ -33,7 +33,7 @@ export class PiboPluginRegistry {
|
|
|
33
33
|
capabilityPackages = new Map();
|
|
34
34
|
eventListeners = new Set();
|
|
35
35
|
productEventListeners = new Set();
|
|
36
|
-
|
|
36
|
+
loopStopConditions = new Map();
|
|
37
37
|
pluginIds = new Set();
|
|
38
38
|
pluginNames = new Map();
|
|
39
39
|
eventErrors = [];
|
|
@@ -133,20 +133,22 @@ export class PiboPluginRegistry {
|
|
|
133
133
|
registerCapabilityPackage(pkg) {
|
|
134
134
|
this.addUnique(this.capabilityPackages, pkg.name, { ...pkg, toolNames: [...pkg.toolNames] }, "capability package");
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
registerLoopStopCondition(condition, pluginId) {
|
|
137
137
|
if (!condition.type.trim())
|
|
138
|
-
throw new Error('
|
|
138
|
+
throw new Error('Loop stop condition type is required');
|
|
139
139
|
if (!condition.name.trim())
|
|
140
|
-
throw new Error(`
|
|
140
|
+
throw new Error(`Loop stop condition "${condition.type}" name is required`);
|
|
141
141
|
if (condition.phases.length === 0)
|
|
142
|
-
throw new Error(`
|
|
143
|
-
this.addUnique(this.
|
|
142
|
+
throw new Error(`Loop stop condition "${condition.type}" must support at least one phase`);
|
|
143
|
+
this.addUnique(this.loopStopConditions, condition.type, { definition: { ...condition, phases: [...condition.phases] }, pluginId }, 'Loop stop condition');
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
registerRalphStopCondition(condition, pluginId) { this.registerLoopStopCondition(condition, pluginId); }
|
|
146
|
+
getLoopStopConditionDefinitions() {
|
|
147
|
+
return [...this.loopStopConditions.values()].map((entry) => entry.definition);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
getRalphStopConditionDefinitions() { return this.getLoopStopConditionDefinitions(); }
|
|
150
|
+
getLoopStopConditionInfos() {
|
|
151
|
+
return [...this.loopStopConditions.values()].map((entry) => ({
|
|
150
152
|
type: entry.definition.type,
|
|
151
153
|
name: entry.definition.name,
|
|
152
154
|
description: entry.definition.description,
|
|
@@ -157,6 +159,7 @@ export class PiboPluginRegistry {
|
|
|
157
159
|
pluginName: entry.pluginId ? this.pluginNames.get(entry.pluginId) : undefined,
|
|
158
160
|
}));
|
|
159
161
|
}
|
|
162
|
+
getRalphStopConditionInfos() { return this.getLoopStopConditionInfos(); }
|
|
160
163
|
onEvent(listener) {
|
|
161
164
|
this.eventListeners.add(listener);
|
|
162
165
|
}
|
|
@@ -203,6 +206,7 @@ export class PiboPluginRegistry {
|
|
|
203
206
|
builtinToolNames: [...sessionContext.builtinToolNames],
|
|
204
207
|
autoContextFiles: sessionContext.autoContextFiles,
|
|
205
208
|
runControl: sessionContext.toolPackages.runControl === true,
|
|
209
|
+
goalControl: sessionContext.toolPackages.goalControl !== false,
|
|
206
210
|
};
|
|
207
211
|
});
|
|
208
212
|
}
|
|
@@ -255,6 +259,11 @@ export class PiboPluginRegistry {
|
|
|
255
259
|
"pibo_run_ack",
|
|
256
260
|
],
|
|
257
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: "pibo-goal-control",
|
|
264
|
+
description: "Expose get_goal, create_goal, and update_goal as one native goal lifecycle package.",
|
|
265
|
+
toolNames: ["get_goal", "create_goal", "update_goal"],
|
|
266
|
+
},
|
|
258
267
|
...[...this.capabilityPackages.values()].map((pkg) => ({
|
|
259
268
|
...pkg,
|
|
260
269
|
toolNames: [...pkg.toolNames],
|
|
@@ -264,7 +273,8 @@ export class PiboPluginRegistry {
|
|
|
264
273
|
piboTools: listInstalledCliToolAgentContexts(),
|
|
265
274
|
mcpServers: [],
|
|
266
275
|
piPackages: listPiPackages(),
|
|
267
|
-
|
|
276
|
+
loopStopConditions: this.getLoopStopConditionInfos(),
|
|
277
|
+
ralphStopConditions: this.getLoopStopConditionInfos(),
|
|
268
278
|
};
|
|
269
279
|
}
|
|
270
280
|
resolveProfileName(name) {
|
|
@@ -355,7 +365,8 @@ export class PiboPluginRegistry {
|
|
|
355
365
|
registerAuthService: (service) => this.registerAuthService(service),
|
|
356
366
|
registerWebApp: (app) => this.registerWebApp(app),
|
|
357
367
|
registerCapabilityPackage: (pkg) => this.registerCapabilityPackage(withPluginPackageContext(pkg)),
|
|
358
|
-
|
|
368
|
+
registerLoopStopCondition: (condition) => this.registerLoopStopCondition(condition, pluginId),
|
|
369
|
+
registerRalphStopCondition: (condition) => this.registerLoopStopCondition(condition, pluginId),
|
|
359
370
|
onEvent: (listener) => this.onEvent(listener),
|
|
360
371
|
emitProductEvent: (event) => this.emitProductEvent(event),
|
|
361
372
|
onProductEvent: (listener) => this.onProductEvent(listener),
|
|
@@ -243,15 +243,13 @@ function buildBrowserReapPlanItem(record, now, idleTimeoutMinutes) {
|
|
|
243
243
|
preservesWorktree: true,
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
async function planComputeReapSafely(options) {
|
|
246
|
+
export async function planComputeReapSafely(options, planCompute = planReapWorkers) {
|
|
247
247
|
try {
|
|
248
|
-
return await
|
|
248
|
+
return await planCompute(options);
|
|
249
249
|
}
|
|
250
|
-
catch
|
|
251
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
252
|
-
throw error;
|
|
250
|
+
catch {
|
|
253
251
|
const plan = buildComputeWorkerReapPlan([], options);
|
|
254
|
-
plan.nextCommands = ["Docker
|
|
252
|
+
plan.nextCommands = ["Docker compute cleanup is unavailable; browser and stale-file cleanup remain active."];
|
|
255
253
|
return plan;
|
|
256
254
|
}
|
|
257
255
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname } from "node:path";
|
|
@@ -5,11 +6,37 @@ import { piboHomePath } from "../core/pibo-home.js";
|
|
|
5
6
|
export function defaultResourceReaperStatePath() {
|
|
6
7
|
return process.env.PIBO_RESOURCE_REAPER_STATE_PATH || piboHomePath("resource-reaper-state.json");
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
+
const DEFAULT_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200];
|
|
10
|
+
const TRANSIENT_RENAME_ERROR_CODES = new Set(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
|
|
11
|
+
export async function writeResourceReaperState(path, state, options = {}) {
|
|
9
12
|
await mkdir(dirname(path), { recursive: true });
|
|
10
|
-
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
13
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
14
|
+
const renameFile = options.rename ?? rename;
|
|
15
|
+
const wait = options.wait ?? defaultWait;
|
|
16
|
+
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RENAME_RETRY_DELAYS_MS;
|
|
11
17
|
await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
12
|
-
|
|
18
|
+
try {
|
|
19
|
+
for (let attempt = 0;; attempt += 1) {
|
|
20
|
+
try {
|
|
21
|
+
await renameFile(temporaryPath, path);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!isTransientRenameError(error) || attempt >= retryDelaysMs.length)
|
|
26
|
+
throw error;
|
|
27
|
+
await wait(retryDelaysMs[attempt]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isTransientRenameError(error) {
|
|
36
|
+
return error instanceof Error && "code" in error && TRANSIENT_RENAME_ERROR_CODES.has(String(error.code));
|
|
37
|
+
}
|
|
38
|
+
async function defaultWait(delayMs) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
13
40
|
}
|
|
14
41
|
export async function claimResourceReaperOwnership(lockPath, pid = process.pid, isPidAlive = defaultIsPidAlive) {
|
|
15
42
|
await mkdir(dirname(lockPath), { recursive: true });
|