oakode 0.0.0-stage → 0.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +9 -0
  3. package/README.md +245 -2
  4. package/SECURITY.md +13 -0
  5. package/dist/account.js +34 -0
  6. package/dist/agent.js +584 -0
  7. package/dist/agents/approval.js +116 -0
  8. package/dist/agents/budget.js +293 -0
  9. package/dist/agents/child.js +103 -0
  10. package/dist/agents/host.js +289 -0
  11. package/dist/agents/orchestrator.js +286 -0
  12. package/dist/agents/plan.js +143 -0
  13. package/dist/agents/types.js +20 -0
  14. package/dist/agents/verify.js +94 -0
  15. package/dist/app.js +619 -0
  16. package/dist/auth.js +456 -0
  17. package/dist/brief.js +97 -0
  18. package/dist/commands.js +588 -0
  19. package/dist/compact.js +231 -0
  20. package/dist/config.js +87 -0
  21. package/dist/diff.js +98 -0
  22. package/dist/doctor.js +106 -0
  23. package/dist/errors.js +15 -0
  24. package/dist/fileio.js +688 -0
  25. package/dist/input.js +244 -0
  26. package/dist/jobs.js +262 -0
  27. package/dist/main.js +560 -0
  28. package/dist/mask.js +63 -0
  29. package/dist/mcp.js +721 -0
  30. package/dist/models.js +106 -0
  31. package/dist/modes.js +114 -0
  32. package/dist/oicki.js +475 -0
  33. package/dist/output.js +101 -0
  34. package/dist/permissions.js +389 -0
  35. package/dist/permissions_ui.js +134 -0
  36. package/dist/print.js +76 -0
  37. package/dist/proc.js +63 -0
  38. package/dist/runners.js +264 -0
  39. package/dist/serial.js +9 -0
  40. package/dist/session.js +452 -0
  41. package/dist/shell.js +40 -0
  42. package/dist/skills.js +335 -0
  43. package/dist/syntax.js +80 -0
  44. package/dist/tools.js +480 -0
  45. package/dist/tui/editor.js +279 -0
  46. package/dist/tui/highlight.js +78 -0
  47. package/dist/tui/markdown.js +254 -0
  48. package/dist/tui/screen.js +146 -0
  49. package/dist/tui/tui.js +505 -0
  50. package/dist/tui/view.js +337 -0
  51. package/dist/ui.js +105 -0
  52. package/dist/update.js +64 -0
  53. package/package.json +57 -4
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Plans: what the parent model proposes with `delegate`, normalized and checked in code before anyone approves it
3
+ * (docs/design/subagents.md §7). The model proposes; this decides what is allowed.
4
+ */
5
+ import { accessNote, allowedModel, effectiveModel, notSupported } from "../models.js";
6
+ import { estimablePricing, estimateAs, worstCaseUsd } from "./budget.js";
7
+ import { EFFORT_CHOICES } from "./types.js";
8
+ const str = (v, max) => (typeof v === "string" ? v.trim().slice(0, max) : "");
9
+ /** The model's `none`/`minimal` become low and `xhigh` becomes high; anything else unknown is the provider's default. */
10
+ function effortOf(v) {
11
+ const s = str(v, 20).toLowerCase();
12
+ if (!s)
13
+ return { effort: "default" };
14
+ if (EFFORT_CHOICES.includes(s))
15
+ return { effort: s };
16
+ if (s === "none" || s === "minimal")
17
+ return { effort: "low", mapped: s };
18
+ if (s === "xhigh")
19
+ return { effort: "high", mapped: s };
20
+ return { effort: "default", mapped: s };
21
+ }
22
+ /**
23
+ * The `delegate` tool's arguments as a plan. Unknown fields are ignored, ids are made safe and unique, the model
24
+ * defaults to `auto`, and each agent's budget is an even share of what is left of the turn's budget.
25
+ */
26
+ export function parsePlan(args, remaining) {
27
+ const notes = [];
28
+ const list = Array.isArray(args?.agents) ? args.agents : [];
29
+ const seen = new Set();
30
+ const agents = list.map((a, i) => {
31
+ let id = str(a?.id, 24).replace(/[^A-Za-z0-9_-]/g, "") || `a${i + 1}`;
32
+ while (seen.has(id))
33
+ id = `${id}_${i + 1}`;
34
+ seen.add(id);
35
+ const { effort, mapped } = effortOf(a?.effort);
36
+ if (mapped)
37
+ notes.push({ agent: id, severity: "warn", text: `effort "${mapped}" is shown as ${effort} (the card offers default, low, medium, high)` });
38
+ return {
39
+ id,
40
+ role: str(a?.role, 120) || `agent ${i + 1}`,
41
+ task: str(a?.task, 8000),
42
+ model: str(a?.model, 200) || "auto",
43
+ effort,
44
+ profile: "read",
45
+ budgetCredits: 0,
46
+ dependsOn: Array.isArray(a?.dependsOn) ? a.dependsOn.map((d) => str(d, 24)).filter(Boolean) : [],
47
+ };
48
+ });
49
+ const share = agents.length ? Math.floor((remaining / agents.length) * 100) / 100 : 0;
50
+ for (const a of agents)
51
+ a.budgetCredits = share;
52
+ return { plan: { goal: str(args?.goal, 500), agents }, notes };
53
+ }
54
+ /** The effort actually sent for a model: nothing for `default` or a model that takes no effort setting. */
55
+ export function effortSent(m, e) {
56
+ return e !== "default" && m?.oicki.capabilities.reasoning ? e : undefined;
57
+ }
58
+ /** Checks a plan; errors block approval until the plan is edited. */
59
+ export function validatePlan(plan, ctx) {
60
+ const issues = [];
61
+ const err = (text, agent) => issues.push({ agent, severity: "error", text });
62
+ const warn = (text, agent) => issues.push({ agent, severity: "warn", text });
63
+ const find = (id) => ctx.catalog.find((m) => m.id === id);
64
+ if (!plan.agents.length)
65
+ err("the plan has no agents");
66
+ if (plan.agents.length > ctx.settings.maxAgents)
67
+ err(`${plan.agents.length} agents; at most ${ctx.settings.maxAgents} (subagents.maxAgents)`);
68
+ const ids = new Set(plan.agents.map((a) => a.id));
69
+ const estimate = {};
70
+ let sum = 0;
71
+ for (const a of plan.agents) {
72
+ if (!a.task)
73
+ err("no task", a.id);
74
+ const m = find(a.model);
75
+ if (!allowedModel(a.model))
76
+ err(notSupported(a.model), a.id);
77
+ else if (!m)
78
+ err(`"${a.model}" is not in OICKI's model list`, a.id);
79
+ else {
80
+ const served = m.oicki.resolves_to ? find(m.oicki.resolves_to) : undefined;
81
+ const eff = effectiveModel(m, served);
82
+ if (!eff.oicki.capabilities.tools)
83
+ err(`${a.model} cannot use tools`, a.id);
84
+ const note = accessNote(m);
85
+ if (note)
86
+ err(`${a.model} ${note}, so OICKI would refuse it for this sign-in`, a.id);
87
+ if ((eff.oicki.context_window ?? Infinity) < 32_000)
88
+ warn(`${a.model} has a small context window (${eff.oicki.context_window} tokens)`, a.id);
89
+ if (a.effort !== "default" && !eff.oicki.capabilities.reasoning)
90
+ warn(`${a.model} takes no effort setting; "${a.effort}" is not sent`, a.id);
91
+ const as = estimateAs(m, find); // a router is estimated as the model it resolves to
92
+ const p = estimablePricing(as);
93
+ if (!as || !p)
94
+ err(`${a.model} has no verified price in the catalog, so its cost cannot be estimated`, a.id);
95
+ else
96
+ estimate[a.id] = estimateAgent(p, ctx.rate, as.id, a.budgetCredits, ctx.settings.agentMaxRequests);
97
+ }
98
+ if (!(a.budgetCredits > 0))
99
+ err("budget must be more than 0 credits", a.id);
100
+ sum += a.budgetCredits;
101
+ for (const d of a.dependsOn)
102
+ if (!ids.has(d))
103
+ err(`depends on "${d}", which is not in the plan`, a.id);
104
+ if (a.dependsOn.includes(a.id))
105
+ err("depends on itself", a.id);
106
+ }
107
+ if (plan.agents.length === 1)
108
+ warn("one agent runs nothing in parallel; doing it yourself is usually cheaper");
109
+ if (sum > ctx.remaining + 1e-9)
110
+ warn(`the agents' budgets add up to ${sum.toFixed(2)} credits, more than the ${ctx.remaining.toFixed(2)} left this turn: they share what is left`);
111
+ const levels = dependencyLevels(plan.agents);
112
+ if (!levels)
113
+ err("the dependencies form a cycle");
114
+ else if (levels.length > 4)
115
+ err(`${levels.length} dependency levels; at most 4`);
116
+ return { plan, issues, estimate, levels: levels ?? [] };
117
+ }
118
+ /** Agents grouped by dependency depth (all of a level can run together), or undefined for a cycle. */
119
+ export function dependencyLevels(agents) {
120
+ const known = new Set(agents.map((a) => a.id));
121
+ const placed = new Set();
122
+ const levels = [];
123
+ while (placed.size < agents.length) {
124
+ const next = agents.filter((a) => !placed.has(a.id) && a.dependsOn.every((d) => placed.has(d) || !known.has(d))).map((a) => a.id);
125
+ if (!next.length)
126
+ return undefined;
127
+ next.forEach((id) => placed.add(id));
128
+ levels.push(next);
129
+ }
130
+ return levels;
131
+ }
132
+ /**
133
+ * A rough range: low = 6 requests of an 8K-token prompt, 70% cached, 600 output tokens; high = the worst case of the
134
+ * agent's request limit (agentMaxRequests), never more than its budget. Both at the model's learned credits-per-USD ratio.
135
+ */
136
+ function estimateAgent(p, rate, model, budget, maxRequests) {
137
+ const perUsd = rate.get(model);
138
+ const cached = p.cache_read_usd_per_million ?? p.input_usd_per_million;
139
+ const lowUsd = 6 * ((8000 * 0.3 * p.input_usd_per_million + 8000 * 0.7 * cached + 600 * p.output_usd_per_million) / 1e6 + (p.per_request_usd ?? 0));
140
+ const highUsd = maxRequests * worstCaseUsd(p, 20_000, 4096);
141
+ const low = lowUsd * perUsd;
142
+ return { low: Math.min(low, budget), high: Math.max(Math.min(highUsd * perUsd, budget), Math.min(low, budget)), firstUse: !rate.known(model) };
143
+ }
@@ -0,0 +1,20 @@
1
+ /** Subagent types and settings (docs/design/subagents.md §3, §5). */
2
+ import { loadConfig } from "../config.js";
3
+ export const EFFORT_CHOICES = ["default", "low", "medium", "high"];
4
+ /** The highest budget OAKODE accepts at all, whatever the config says. */
5
+ export const BUDGET_CEILING = 200;
6
+ const num = (v, def, min, max) => (typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : def);
7
+ /** `"subagents"` in config.json, with defaults and bounds; off unless `"enabled": true`. */
8
+ export function subagentSettings(raw = loadConfig().subagents) {
9
+ const c = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
10
+ const maxBudgetCredits = num(c.maxBudgetCredits, BUDGET_CEILING, 1, BUDGET_CEILING);
11
+ return {
12
+ enabled: c.enabled === true,
13
+ maxParallel: Math.round(num(c.maxParallel, 3, 1, 8)),
14
+ maxAgents: Math.round(num(c.maxAgents, 6, 1, 12)),
15
+ budgetCredits: num(c.budgetCredits, 40, 1, maxBudgetCredits),
16
+ maxBudgetCredits,
17
+ agentTimeoutSec: num(c.agentTimeoutSec, 600, 30, 3600),
18
+ agentMaxRequests: Math.round(num(c.agentMaxRequests, 30, 2, 100)),
19
+ };
20
+ }
@@ -0,0 +1,94 @@
1
+ import { posix } from "node:path";
2
+ /** One spelling per relative path ("./a/../b.ts" and "b.ts" are the same file); absolute paths stay as they are. */
3
+ const norm = (p) => posix.normalize(p.replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/+$/, "");
4
+ /** For each file: the line numbers the agent's tools actually showed it (an empty set: touched, no lines seen). */
5
+ export function seenLines(raw) {
6
+ const seen = new Map();
7
+ const add = (path, n) => {
8
+ const k = norm(path);
9
+ if (!seen.has(k))
10
+ seen.set(k, new Set());
11
+ if (n !== undefined)
12
+ seen.get(k).add(n);
13
+ };
14
+ const results = new Map();
15
+ // text, or (from an older session) a list of text parts
16
+ const text = (c) => (typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => (p && typeof p.text === "string" ? p.text : "")).join("") : "");
17
+ for (const m of raw)
18
+ if (m.role === "tool")
19
+ results.set(m.tool_call_id, text(m.content));
20
+ for (const m of raw) {
21
+ if (m.role !== "assistant" || !m.tool_calls)
22
+ continue;
23
+ for (const c of m.tool_calls) {
24
+ const out = results.get(c.id);
25
+ if (!out || out.startsWith("Error:") || out.startsWith("Interrupted:"))
26
+ continue;
27
+ let args;
28
+ try {
29
+ args = JSON.parse(c.function.arguments);
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ if (c.function.name === "read") {
35
+ // one file: numbered lines; several: "==> path <==" sections, each of which may be an error instead
36
+ const single = Array.isArray(args.paths) ? (args.paths.length === 1 ? args.paths[0] : undefined) : args.path;
37
+ let current = typeof single === "string" ? single : undefined;
38
+ let fresh = false; // just after a section header: its first line decides whether the file was read
39
+ if (current)
40
+ add(current);
41
+ for (const line of out.split("\n")) {
42
+ const head = /^==> (.+) <==$/.exec(line);
43
+ if (head) {
44
+ current = head[1];
45
+ fresh = true;
46
+ continue;
47
+ }
48
+ if (fresh && current) {
49
+ fresh = false;
50
+ if (line.startsWith("Error:")) {
51
+ current = undefined; // "No such file" and the like: not read
52
+ continue;
53
+ }
54
+ add(current);
55
+ }
56
+ const num = /^\s*(\d+)\t/.exec(line);
57
+ if (current && num)
58
+ add(current, Number(num[1]));
59
+ }
60
+ }
61
+ else if (c.function.name === "grep") {
62
+ for (const line of out.split("\n")) {
63
+ const hit = /^(.+?)[:-](\d+)[:-] /.exec(line);
64
+ if (hit)
65
+ add(hit[1], Number(hit[2]));
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return seen;
71
+ }
72
+ export function verifyReport(report, raw) {
73
+ const seen = seenLines(raw);
74
+ const check = (f) => {
75
+ if (!f.evidence.length)
76
+ return { ...f, verified: false, why: "no evidence cited" };
77
+ for (const e of f.evidence) {
78
+ if (e.command)
79
+ return { ...f, verified: false, why: `cites a command (${e.command.slice(0, 60)}) it could not have run` };
80
+ if (!e.path)
81
+ return { ...f, verified: false, why: "evidence without a file" };
82
+ const lines = seen.get(norm(e.path));
83
+ if (!lines)
84
+ return { ...f, verified: false, why: `cites ${e.path}, which it never read` };
85
+ if (e.lines) {
86
+ for (let n = e.lines[0]; n <= e.lines[1]; n++)
87
+ if (!lines.has(n))
88
+ return { ...f, verified: false, why: `cites ${e.path}:${e.lines[0]}-${e.lines[1]}, but line ${n} was never shown to it` };
89
+ }
90
+ }
91
+ return { ...f, verified: true };
92
+ };
93
+ return { ...report, findings: report.findings.map(check) };
94
+ }