@alisio/core 0.1.0-alpha.9 → 0.1.1
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 +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +105 -3
- package/dist/application.js +446 -18
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +306 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +206 -6
- package/dist/config.js +426 -20
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +134 -4
- package/dist/core/runner.js +780 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +50 -5
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +34 -1
- package/dist/plugins/host.js +143 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { textResult } from "@alisio/sdk";
|
|
2
|
+
import { EXIT_PLAN_TOOL } from "../core/opt-in.js";
|
|
3
|
+
import { objectSchema } from "../tools/standard.js";
|
|
4
|
+
import { decidePlan, nextPlanRevision, PLAN_CONTEXT_MAX_CHARS, PLAN_MAX_CHARS, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, planTitle, proposePlan, withdrawPendingPlan, } from "./state.js";
|
|
5
|
+
const withArtifact = (result, artifact) => artifact
|
|
6
|
+
? {
|
|
7
|
+
...result,
|
|
8
|
+
content: [...result.content, { type: "ui", block: { kind: "artifact", artifact } }],
|
|
9
|
+
}
|
|
10
|
+
: result;
|
|
11
|
+
const answer = (decision, message, extra = {}) => JSON.stringify({ decision, ...extra, message });
|
|
12
|
+
export function registerExitPlan(registry, deps) {
|
|
13
|
+
registry.register({
|
|
14
|
+
name: EXIT_PLAN_TOOL,
|
|
15
|
+
effect: "read",
|
|
16
|
+
description: "Finish planning: hand the complete plan, in Markdown, to the user for review. Call it once, " +
|
|
17
|
+
"when the plan is complete, instead of pasting the plan in your reply. The user can approve " +
|
|
18
|
+
"(the build agent then implements exactly this plan), skip for now, or add context (the " +
|
|
19
|
+
"result is `feedback`: revise the plan and call exit_plan again). Only the plan agent has " +
|
|
20
|
+
"this tool. If review is unavailable (headless or read-only sessions) the result says so: " +
|
|
21
|
+
"then give the complete plan as your final reply.",
|
|
22
|
+
inputSchema: objectSchema({
|
|
23
|
+
title: { type: "string", maxLength: 120, description: "Short plan title" },
|
|
24
|
+
plan: {
|
|
25
|
+
type: "string",
|
|
26
|
+
minLength: 1,
|
|
27
|
+
maxLength: PLAN_MAX_CHARS,
|
|
28
|
+
description: "The whole plan in Markdown",
|
|
29
|
+
},
|
|
30
|
+
}, ["plan"]),
|
|
31
|
+
async execute(input, context) {
|
|
32
|
+
const plan = String(input.plan ?? "").trim();
|
|
33
|
+
if (!plan)
|
|
34
|
+
throw new Error("exit_plan needs the whole plan in the `plan` field");
|
|
35
|
+
const sessionId = context.session;
|
|
36
|
+
const title = planTitle(plan, input.title);
|
|
37
|
+
const hash = planHash(plan);
|
|
38
|
+
const planId = crypto.randomUUID();
|
|
39
|
+
const reviewable = !!sessionId && deps.ui.interactive() && !deps.readOnly;
|
|
40
|
+
const revision = sessionId ? nextPlanRevision(deps.store, sessionId) : 1;
|
|
41
|
+
// The plan is saved first, so the review (and every later turn) can point at it.
|
|
42
|
+
let artifact;
|
|
43
|
+
let publishNote = "";
|
|
44
|
+
if (context.artifacts)
|
|
45
|
+
try {
|
|
46
|
+
artifact = await context.artifacts.publishText({
|
|
47
|
+
fileName: "plan.md",
|
|
48
|
+
title: revision > 1 ? `${title} (revision ${revision})` : title,
|
|
49
|
+
text: `${plan}\n`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
publishNote = ` The plan could not be saved as an artifact: ${error instanceof Error ? error.message : String(error)}.`;
|
|
54
|
+
}
|
|
55
|
+
const record = {
|
|
56
|
+
planId,
|
|
57
|
+
revision,
|
|
58
|
+
hash,
|
|
59
|
+
title,
|
|
60
|
+
...(context.callId ? { callId: context.callId } : {}),
|
|
61
|
+
...(artifact ? { artifactId: artifact.id } : {}),
|
|
62
|
+
};
|
|
63
|
+
if (!reviewable) {
|
|
64
|
+
// Nobody to ask: keep the revision count honest and tell the model what to do instead.
|
|
65
|
+
if (sessionId)
|
|
66
|
+
proposePlan(deps.store, sessionId, { ...record, status: "skipped" });
|
|
67
|
+
return withArtifact(textResult(answer("unavailable", `No plan review is possible in this session (${deps.readOnly ? "it is read-only" : "it is not interactive"}), so nothing will be implemented from here. Give the complete plan in Markdown as your final reply and stop.${publishNote}`, artifact ? { artifactId: artifact.id } : {})), artifact);
|
|
68
|
+
}
|
|
69
|
+
proposePlan(deps.store, sessionId, record);
|
|
70
|
+
context.emitEvent?.("plan_proposed", {
|
|
71
|
+
callId: context.callId ?? "",
|
|
72
|
+
planId,
|
|
73
|
+
revision,
|
|
74
|
+
hash,
|
|
75
|
+
title,
|
|
76
|
+
...(artifact ? { artifactId: artifact.id } : {}),
|
|
77
|
+
});
|
|
78
|
+
const review = {
|
|
79
|
+
planId,
|
|
80
|
+
revision,
|
|
81
|
+
hash,
|
|
82
|
+
title,
|
|
83
|
+
markdown: plan,
|
|
84
|
+
...(artifact ? { artifact } : {}),
|
|
85
|
+
};
|
|
86
|
+
const result = await deps.ui.askQuestions({
|
|
87
|
+
questions: [
|
|
88
|
+
{
|
|
89
|
+
id: PLAN_QUESTION_ID,
|
|
90
|
+
header: "Plan review",
|
|
91
|
+
question: PLAN_REVIEW_TITLE,
|
|
92
|
+
options: PLAN_OPTIONS,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
plan: review,
|
|
96
|
+
session: sessionId,
|
|
97
|
+
...(context.label ? { label: context.label } : {}),
|
|
98
|
+
signal: context.signal,
|
|
99
|
+
});
|
|
100
|
+
const choice = result[PLAN_QUESTION_ID];
|
|
101
|
+
const raw = result[PLAN_TEXT_KEY];
|
|
102
|
+
const text = (typeof raw === "string" ? raw : "").trim().slice(0, PLAN_CONTEXT_MAX_CHARS);
|
|
103
|
+
// Anything but a clear answer (Esc, closed screen, cancelled run, empty context) is a skip.
|
|
104
|
+
const decision = choice === "approve" ? "approve" : choice === "context" && text ? "context" : "skip";
|
|
105
|
+
const decided = decidePlan(deps.store, sessionId, planId, decision, plan);
|
|
106
|
+
if (!decided) {
|
|
107
|
+
// Not pending any more (replaced or settled elsewhere): nothing to apply.
|
|
108
|
+
withdrawPendingPlan(deps.store, sessionId);
|
|
109
|
+
return withArtifact(textResult(answer("skipped", "This plan is no longer awaiting a decision. Stay in plan mode.")), artifact);
|
|
110
|
+
}
|
|
111
|
+
context.emitEvent?.("plan_decided", {
|
|
112
|
+
callId: context.callId ?? "",
|
|
113
|
+
planId,
|
|
114
|
+
hash,
|
|
115
|
+
decision,
|
|
116
|
+
});
|
|
117
|
+
const base = { planId, revision, ...(artifact ? { artifactId: artifact.id } : {}) };
|
|
118
|
+
const body = decision === "approve"
|
|
119
|
+
? answer("approved", "The user approved the plan. The build agent will implement exactly this plan in the " +
|
|
120
|
+
"next turn. Do not implement anything yourself: reply with one short sentence and " +
|
|
121
|
+
"call no more tools.", base)
|
|
122
|
+
: decision === "context"
|
|
123
|
+
? answer("feedback", "The user added context instead of deciding. Revise the plan to account for it and " +
|
|
124
|
+
"call exit_plan again with the complete updated plan.", { ...base, feedback: text })
|
|
125
|
+
: answer("skipped", "The user chose to skip for now. Stay in plan mode and do not implement anything. " +
|
|
126
|
+
"Do not call exit_plan again unless the user asks; end your turn with one short sentence.", base);
|
|
127
|
+
return withArtifact(textResult(body), artifact);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { QuestionOption } from "@alisio/sdk";
|
|
2
|
+
/** The decision screen title (web: localized; the TUI prints it as is). */
|
|
3
|
+
export declare const PLAN_REVIEW_TITLE = "Plan complete. What would you like to do?";
|
|
4
|
+
export declare const PLAN_QUESTION_ID = "plan";
|
|
5
|
+
export declare const PLAN_TEXT_KEY = "plan:text";
|
|
6
|
+
/** The agent an approved plan is handed to. */
|
|
7
|
+
export declare const IMPLEMENTATION_AGENT_ID = "build";
|
|
8
|
+
export declare const PLAN_MAX_CHARS = 60000;
|
|
9
|
+
/** Longest "Add context" text accepted (the same bound everywhere). */
|
|
10
|
+
export declare const PLAN_CONTEXT_MAX_CHARS = 20000;
|
|
11
|
+
/** The three decisions, with the exact visible English strings (the web localizes by `value`). */
|
|
12
|
+
export declare const PLAN_OPTIONS: QuestionOption[];
|
|
13
|
+
export type PlanStatus = "pending" | "approved" | "implementing" | "skipped" | "feedback" | "cancelled";
|
|
14
|
+
export interface PlanState {
|
|
15
|
+
/** One per `exit_plan` call. */
|
|
16
|
+
planId: string;
|
|
17
|
+
revision: number;
|
|
18
|
+
hash: string;
|
|
19
|
+
title: string;
|
|
20
|
+
status: PlanStatus;
|
|
21
|
+
callId?: string;
|
|
22
|
+
artifactId?: string;
|
|
23
|
+
/** The approved snapshot; kept only while `approved` (afterwards it lives in the user message). */
|
|
24
|
+
plan?: string;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
27
|
+
/** What a store must offer to keep plan state (`SQLiteStore` does). */
|
|
28
|
+
export interface PlanStore {
|
|
29
|
+
mutateSessionOptions<T>(id: string, change: (current: Record<string, unknown>) => {
|
|
30
|
+
patch?: Record<string, unknown>;
|
|
31
|
+
value: T;
|
|
32
|
+
}): T;
|
|
33
|
+
get(id: string): {
|
|
34
|
+
options?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare const planHash: (plan: string) => string;
|
|
38
|
+
/** The plan state stored in `options`, or undefined when absent or malformed. */
|
|
39
|
+
export declare function readPlanState(options: Record<string, unknown> | undefined): PlanState | undefined;
|
|
40
|
+
export declare const currentPlan: (store: PlanStore, sessionId: string) => PlanState | undefined;
|
|
41
|
+
/** The revision the next proposal of this session gets. */
|
|
42
|
+
export declare const nextPlanRevision: (store: PlanStore, sessionId: string) => number;
|
|
43
|
+
/** Records a new proposal (replacing any earlier one: only the latest plan can be decided). */
|
|
44
|
+
export declare function proposePlan(store: PlanStore, sessionId: string, input: Omit<PlanState, "status" | "updatedAt" | "plan"> & {
|
|
45
|
+
status?: PlanStatus;
|
|
46
|
+
}): PlanState;
|
|
47
|
+
export type PlanDecision = "approve" | "skip" | "context";
|
|
48
|
+
/**
|
|
49
|
+
* Applies the user's decision to the pending proposal `planId`. Returns the new state, or
|
|
50
|
+
* undefined when that proposal is not pending any more (already decided, replaced, withdrawn).
|
|
51
|
+
*/
|
|
52
|
+
export declare function decidePlan(store: PlanStore, sessionId: string, planId: string, decision: PlanDecision, plan: string): PlanState | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Hands an approved plan to the implementation turn, exactly once: `approved` becomes
|
|
55
|
+
* `implementing` and `alsoSet` is merged into the session options in the same transaction (the
|
|
56
|
+
* web switches the session to `build` there). A second call finds nothing and returns undefined.
|
|
57
|
+
*/
|
|
58
|
+
export declare function claimApprovedPlan(store: PlanStore, sessionId: string, alsoSet?: Record<string, unknown>): (PlanState & {
|
|
59
|
+
plan: string;
|
|
60
|
+
}) | undefined;
|
|
61
|
+
/** An approved plan whose run was cancelled never starts anything: it is marked `cancelled`. */
|
|
62
|
+
export declare function discardApprovedPlan(store: PlanStore, sessionId: string): boolean;
|
|
63
|
+
/** A proposal still `pending` when its run ends was never answered: it counts as skipped. */
|
|
64
|
+
export declare function withdrawPendingPlan(store: PlanStore, sessionId: string): boolean;
|
|
65
|
+
export type PlanFollowUp = {
|
|
66
|
+
kind: "none";
|
|
67
|
+
}
|
|
68
|
+
/** An approval the run's cancellation dropped before anything started. */
|
|
69
|
+
| {
|
|
70
|
+
kind: "dropped";
|
|
71
|
+
}
|
|
72
|
+
/** Start ONE implementation turn with this prompt (the claim already happened). */
|
|
73
|
+
| {
|
|
74
|
+
kind: "implement";
|
|
75
|
+
plan: PlanState & {
|
|
76
|
+
plan: string;
|
|
77
|
+
};
|
|
78
|
+
text: string;
|
|
79
|
+
display: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* What a host does when a run has ended (the same rules for the TUI and the server): a cancelled
|
|
83
|
+
* run drops an approved plan, an unanswered review counts as skipped, and an approved plan is
|
|
84
|
+
* claimed exactly once and turned into the implementation prompt.
|
|
85
|
+
*/
|
|
86
|
+
export declare function settlePlanRun(store: PlanStore, sessionId: string, options: {
|
|
87
|
+
aborted: boolean;
|
|
88
|
+
alsoSet?: Record<string, unknown>;
|
|
89
|
+
}): PlanFollowUp;
|
|
90
|
+
/** The first Markdown heading, as a fallback title. */
|
|
91
|
+
export declare function planTitle(plan: string, given?: unknown): string;
|
|
92
|
+
/**
|
|
93
|
+
* The persisted user message of the implementation turn: the approved plan as an immutable
|
|
94
|
+
* snapshot, plus the short `display` the UIs show instead of the whole plan (they already showed
|
|
95
|
+
* it at review time).
|
|
96
|
+
*/
|
|
97
|
+
export declare function implementationPrompt(state: PlanState & {
|
|
98
|
+
plan: string;
|
|
99
|
+
}): {
|
|
100
|
+
text: string;
|
|
101
|
+
display: string;
|
|
102
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan review state (`sessions.options.plan`) and the pure helpers around it. One entry per
|
|
3
|
+
* session: the latest `exit_plan` proposal. Every transition is a compare-and-set inside one
|
|
4
|
+
* `BEGIN IMMEDIATE` transaction, so it is safe across surfaces (and processes) that share the
|
|
5
|
+
* database: whatever happens twice (a double click, a reload, two tabs) changes the state once.
|
|
6
|
+
*
|
|
7
|
+
* pending ──approve──▶ approved ──claim──▶ implementing
|
|
8
|
+
* ├────skip──────▶ skipped └─discard─▶ cancelled (the run was cancelled)
|
|
9
|
+
* └────context───▶ feedback (a later `exit_plan` call starts a new revision)
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/** The decision screen title (web: localized; the TUI prints it as is). */
|
|
13
|
+
export const PLAN_REVIEW_TITLE = "Plan complete. What would you like to do?";
|
|
14
|
+
export const PLAN_QUESTION_ID = "plan";
|
|
15
|
+
export const PLAN_TEXT_KEY = `${PLAN_QUESTION_ID}:text`;
|
|
16
|
+
/** The agent an approved plan is handed to. */
|
|
17
|
+
export const IMPLEMENTATION_AGENT_ID = "build";
|
|
18
|
+
export const PLAN_MAX_CHARS = 60_000;
|
|
19
|
+
/** Longest "Add context" text accepted (the same bound everywhere). */
|
|
20
|
+
export const PLAN_CONTEXT_MAX_CHARS = 20_000;
|
|
21
|
+
/** The three decisions, with the exact visible English strings (the web localizes by `value`). */
|
|
22
|
+
export const PLAN_OPTIONS = [
|
|
23
|
+
{ value: "approve", label: "Agree and start implementation", recommended: true },
|
|
24
|
+
{ value: "skip", label: "Skip for now" },
|
|
25
|
+
{
|
|
26
|
+
value: "context",
|
|
27
|
+
label: "Add context",
|
|
28
|
+
textInput: { placeholder: "Add context for the plan" },
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
export const planHash = (plan) => createHash("sha256").update(plan, "utf8").digest("hex");
|
|
32
|
+
const STATUSES = new Set([
|
|
33
|
+
"pending",
|
|
34
|
+
"approved",
|
|
35
|
+
"implementing",
|
|
36
|
+
"skipped",
|
|
37
|
+
"feedback",
|
|
38
|
+
"cancelled",
|
|
39
|
+
]);
|
|
40
|
+
/** The plan state stored in `options`, or undefined when absent or malformed. */
|
|
41
|
+
export function readPlanState(options) {
|
|
42
|
+
const raw = options?.plan;
|
|
43
|
+
if (!raw ||
|
|
44
|
+
typeof raw !== "object" ||
|
|
45
|
+
typeof raw.planId !== "string" ||
|
|
46
|
+
typeof raw.hash !== "string" ||
|
|
47
|
+
typeof raw.revision !== "number" ||
|
|
48
|
+
typeof raw.status !== "string" ||
|
|
49
|
+
!STATUSES.has(raw.status))
|
|
50
|
+
return undefined;
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
export const currentPlan = (store, sessionId) => readPlanState(store.get(sessionId).options);
|
|
54
|
+
/** The revision the next proposal of this session gets. */
|
|
55
|
+
export const nextPlanRevision = (store, sessionId) => (currentPlan(store, sessionId)?.revision ?? 0) + 1;
|
|
56
|
+
/** Records a new proposal (replacing any earlier one: only the latest plan can be decided). */
|
|
57
|
+
export function proposePlan(store, sessionId, input) {
|
|
58
|
+
const state = { ...input, status: input.status ?? "pending", updatedAt: Date.now() };
|
|
59
|
+
return store.mutateSessionOptions(sessionId, () => ({ patch: { plan: state }, value: state }));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Applies the user's decision to the pending proposal `planId`. Returns the new state, or
|
|
63
|
+
* undefined when that proposal is not pending any more (already decided, replaced, withdrawn).
|
|
64
|
+
*/
|
|
65
|
+
export function decidePlan(store, sessionId, planId, decision, plan) {
|
|
66
|
+
return store.mutateSessionOptions(sessionId, (options) => {
|
|
67
|
+
const current = readPlanState(options);
|
|
68
|
+
if (!current || current.planId !== planId || current.status !== "pending")
|
|
69
|
+
return { value: undefined };
|
|
70
|
+
const next = {
|
|
71
|
+
...current,
|
|
72
|
+
status: decision === "approve" ? "approved" : decision === "skip" ? "skipped" : "feedback",
|
|
73
|
+
...(decision === "approve" ? { plan } : {}),
|
|
74
|
+
updatedAt: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
return { patch: { plan: next }, value: next };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Hands an approved plan to the implementation turn, exactly once: `approved` becomes
|
|
81
|
+
* `implementing` and `alsoSet` is merged into the session options in the same transaction (the
|
|
82
|
+
* web switches the session to `build` there). A second call finds nothing and returns undefined.
|
|
83
|
+
*/
|
|
84
|
+
export function claimApprovedPlan(store, sessionId, alsoSet = {}) {
|
|
85
|
+
return store.mutateSessionOptions(sessionId, (options) => {
|
|
86
|
+
const current = readPlanState(options);
|
|
87
|
+
if (!current || current.status !== "approved" || typeof current.plan !== "string")
|
|
88
|
+
return { value: undefined };
|
|
89
|
+
const { plan, ...rest } = current;
|
|
90
|
+
const claimed = { ...rest, status: "implementing", updatedAt: Date.now() };
|
|
91
|
+
return { patch: { ...alsoSet, plan: claimed }, value: { ...claimed, plan } };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** An approved plan whose run was cancelled never starts anything: it is marked `cancelled`. */
|
|
95
|
+
export function discardApprovedPlan(store, sessionId) {
|
|
96
|
+
return store.mutateSessionOptions(sessionId, (options) => {
|
|
97
|
+
const current = readPlanState(options);
|
|
98
|
+
if (!current || current.status !== "approved")
|
|
99
|
+
return { value: false };
|
|
100
|
+
const { plan: _snapshot, ...rest } = current;
|
|
101
|
+
return {
|
|
102
|
+
patch: { plan: { ...rest, status: "cancelled", updatedAt: Date.now() } },
|
|
103
|
+
value: true,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** A proposal still `pending` when its run ends was never answered: it counts as skipped. */
|
|
108
|
+
export function withdrawPendingPlan(store, sessionId) {
|
|
109
|
+
return store.mutateSessionOptions(sessionId, (options) => {
|
|
110
|
+
const current = readPlanState(options);
|
|
111
|
+
if (!current || current.status !== "pending")
|
|
112
|
+
return { value: false };
|
|
113
|
+
return {
|
|
114
|
+
patch: { plan: { ...current, status: "skipped", updatedAt: Date.now() } },
|
|
115
|
+
value: true,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* What a host does when a run has ended (the same rules for the TUI and the server): a cancelled
|
|
121
|
+
* run drops an approved plan, an unanswered review counts as skipped, and an approved plan is
|
|
122
|
+
* claimed exactly once and turned into the implementation prompt.
|
|
123
|
+
*/
|
|
124
|
+
export function settlePlanRun(store, sessionId, options) {
|
|
125
|
+
withdrawPendingPlan(store, sessionId);
|
|
126
|
+
if (options.aborted)
|
|
127
|
+
return discardApprovedPlan(store, sessionId) ? { kind: "dropped" } : { kind: "none" };
|
|
128
|
+
const claimed = claimApprovedPlan(store, sessionId, options.alsoSet);
|
|
129
|
+
if (!claimed)
|
|
130
|
+
return { kind: "none" };
|
|
131
|
+
return { kind: "implement", plan: claimed, ...implementationPrompt(claimed) };
|
|
132
|
+
}
|
|
133
|
+
/** The first Markdown heading, as a fallback title. */
|
|
134
|
+
export function planTitle(plan, given) {
|
|
135
|
+
const explicit = typeof given === "string" ? given.trim() : "";
|
|
136
|
+
if (explicit)
|
|
137
|
+
return explicit.slice(0, 120);
|
|
138
|
+
const heading = /^#{1,3}\s+(.+?)\s*#*\s*$/m.exec(plan)?.[1]?.trim();
|
|
139
|
+
return (heading || "Implementation plan").slice(0, 120);
|
|
140
|
+
}
|
|
141
|
+
const oneLine = (text, max) => {
|
|
142
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
143
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* The persisted user message of the implementation turn: the approved plan as an immutable
|
|
147
|
+
* snapshot, plus the short `display` the UIs show instead of the whole plan (they already showed
|
|
148
|
+
* it at review time).
|
|
149
|
+
*/
|
|
150
|
+
export function implementationPrompt(state) {
|
|
151
|
+
const artifact = state.artifactId ? `, artifact ${state.artifactId}` : "";
|
|
152
|
+
return {
|
|
153
|
+
text: [
|
|
154
|
+
`Implement the approved plan (revision ${state.revision}, sha256 ${state.hash.slice(0, 12)}${artifact}).`,
|
|
155
|
+
"The plan below is the approved snapshot. Follow it: do not plan again and do not ask for",
|
|
156
|
+
"approval again. If a step turns out to be wrong or impossible, say so and make the smallest",
|
|
157
|
+
"adjustment that keeps the plan's goal.",
|
|
158
|
+
"",
|
|
159
|
+
`<approved_plan title="${state.title.replace(/"/g, "'")}">`,
|
|
160
|
+
state.plan,
|
|
161
|
+
"</approved_plan>",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
display: `Implement the approved plan: ${oneLine(state.title, 100)}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest } from "@alisio/sdk";
|
|
1
|
+
import type { AskQuestionsRequest, AskQuestionsResult, CommandContext, CommandOptions, CompletionRequest, Message, PanelProvider, Plugin, PluginAPI, PluginMetadata, RunEvent, SelectRequest, ViewContext } from "@alisio/sdk";
|
|
2
2
|
import type { HookFailure, RunnerExtensions } from "../core/contracts.ts";
|
|
3
3
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
4
4
|
import { ExtensionRegistry } from "../extensions/registry.ts";
|
|
@@ -10,6 +10,19 @@ export interface PluginHostOptions {
|
|
|
10
10
|
/** Timeout for session-end hooks (for example an automatic summary). */
|
|
11
11
|
sessionEndTimeoutMs?: number;
|
|
12
12
|
}
|
|
13
|
+
/** A failed data view: the host maps `code` to an API error (never echoing parameter values). */
|
|
14
|
+
export declare class ViewRunError extends Error {
|
|
15
|
+
readonly code: "not_found" | "invalid_params" | "failed";
|
|
16
|
+
readonly fields: string[];
|
|
17
|
+
constructor(code: "not_found" | "invalid_params" | "failed", message: string, fields?: string[], options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export interface ViewInfo {
|
|
22
|
+
id: string;
|
|
23
|
+
description: string;
|
|
24
|
+
params: Record<string, unknown>;
|
|
25
|
+
}
|
|
13
26
|
export declare class PluginHost implements RunnerExtensions {
|
|
14
27
|
private registry;
|
|
15
28
|
private state;
|
|
@@ -37,6 +50,8 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
37
50
|
private startHandlers;
|
|
38
51
|
private endHandlers;
|
|
39
52
|
private fieldOwners;
|
|
53
|
+
/** Registered tool name → contributing plugin id. */
|
|
54
|
+
private toolOwners;
|
|
40
55
|
private completer?;
|
|
41
56
|
private modelsImpl?;
|
|
42
57
|
observers: Set<(event: Readonly<RunEvent>) => void>;
|
|
@@ -57,6 +72,8 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
57
72
|
plugin: string;
|
|
58
73
|
dir: string;
|
|
59
74
|
}>;
|
|
75
|
+
/** Read-only data views contributed by plugins, keyed `plugin:view`. */
|
|
76
|
+
private views;
|
|
60
77
|
/** Tree panels contributed by plugins, keyed `plugin:id`. */
|
|
61
78
|
panels: Map<string, {
|
|
62
79
|
plugin: string;
|
|
@@ -72,6 +89,12 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
72
89
|
askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
|
|
73
90
|
open(sessionId: string): boolean;
|
|
74
91
|
}): void;
|
|
92
|
+
/**
|
|
93
|
+
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
94
|
+
* uses this to consume plugin-published data (for example the main-capable agent definitions the
|
|
95
|
+
* built-in subagents plugin publishes) without importing the plugin package.
|
|
96
|
+
*/
|
|
97
|
+
pluginState(plugin: string, key: string): unknown;
|
|
75
98
|
get sessions(): PluginAPI["sessions"];
|
|
76
99
|
/**
|
|
77
100
|
* Live accessor for core standard tools (e.g. `ask_user_question`), which are not "a plugin"
|
|
@@ -160,6 +183,16 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
160
183
|
}>;
|
|
161
184
|
/** Loaded plugins (id, version, built-in flag), ordered by id. */
|
|
162
185
|
metadata(): PluginMetadata[];
|
|
186
|
+
/** Data views a plugin registered (without their handlers). */
|
|
187
|
+
viewsOf(plugin: string): ViewInfo[];
|
|
188
|
+
/**
|
|
189
|
+
* Validates `query` against the view's declared schema (coerced from strings) and runs it.
|
|
190
|
+
* Timeouts and response caps are the caller's job (the server enforces both). Errors are
|
|
191
|
+
* `ViewRunError`s whose messages never include parameter values or handler internals.
|
|
192
|
+
*/
|
|
193
|
+
runView(plugin: string, id: string, query: Record<string, string>, context: ViewContext): Promise<unknown>;
|
|
194
|
+
/** Registered names of the tools a plugin contributes (namespaced unless built in). */
|
|
195
|
+
toolsOf(plugin: string): string[];
|
|
163
196
|
get hasSessionEndHooks(): boolean;
|
|
164
197
|
/** Conservative ownership check used to protect a live session from plugin removal. */
|
|
165
198
|
ownsSessionResources(plugin: string): boolean;
|