@suwujs/king-ai 0.2.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/README.md +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- package/package.json +63 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HostCapsule } from "./host-ledger.js";
|
|
2
|
+
export interface HostExportInput {
|
|
3
|
+
workspaceRoot?: string;
|
|
4
|
+
repoRoot?: string;
|
|
5
|
+
outputDir?: string;
|
|
6
|
+
runId?: string;
|
|
7
|
+
includeWorkspace?: boolean;
|
|
8
|
+
includeRepoPatch?: boolean;
|
|
9
|
+
capsuleId?: string;
|
|
10
|
+
capsulesFile?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface HostExportPlan {
|
|
13
|
+
runId: string;
|
|
14
|
+
outputDir: string;
|
|
15
|
+
exportDir: string;
|
|
16
|
+
workspaceRoot?: string;
|
|
17
|
+
repoRoot?: string;
|
|
18
|
+
includeWorkspace: boolean;
|
|
19
|
+
includeRepoPatch: boolean;
|
|
20
|
+
capsuleId?: string;
|
|
21
|
+
capsule?: HostCapsule;
|
|
22
|
+
workspaceFileCount: number;
|
|
23
|
+
repoDirty: boolean;
|
|
24
|
+
files: string[];
|
|
25
|
+
summary: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HostExportResult extends HostExportPlan {
|
|
28
|
+
writtenFiles: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface HostExportMeta {
|
|
31
|
+
schema: "king-ai.host-export.v1";
|
|
32
|
+
runId: string;
|
|
33
|
+
exportedAt: string;
|
|
34
|
+
outputDir: string;
|
|
35
|
+
exportDir: string;
|
|
36
|
+
workspaceRoot?: string;
|
|
37
|
+
repoRoot?: string;
|
|
38
|
+
includeWorkspace: boolean;
|
|
39
|
+
includeRepoPatch: boolean;
|
|
40
|
+
capsuleId?: string;
|
|
41
|
+
capsule?: HostCapsule;
|
|
42
|
+
workspaceFileCount: number;
|
|
43
|
+
repoDirty: boolean;
|
|
44
|
+
files: string[];
|
|
45
|
+
writtenFiles: string[];
|
|
46
|
+
}
|
|
47
|
+
export declare function planHostExport(input?: HostExportInput): Promise<HostExportPlan>;
|
|
48
|
+
export declare function exportHostArtifacts(input?: HostExportInput): Promise<HostExportResult>;
|
|
49
|
+
export declare function formatHostExportPlan(plan: HostExportPlan): string;
|
|
50
|
+
export declare function createHostExportMeta(plan: HostExportPlan, writtenFiles: string[], now?: Date): HostExportMeta;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { getHostCapsule } from "./host-ledger.js";
|
|
6
|
+
import { isGitRepo } from "./worktree.js";
|
|
7
|
+
export async function planHostExport(input = {}) {
|
|
8
|
+
const explicitRunId = cleanString(input.runId);
|
|
9
|
+
const runId = explicitRunId ? safeFilenameSegment(explicitRunId, "runId") : buildExportRunId();
|
|
10
|
+
const outputDir = resolve(cleanString(input.outputDir) || "deliverables");
|
|
11
|
+
const exportDir = join(outputDir, runId);
|
|
12
|
+
const workspaceRoot = resolveOptionalExistingDir(input.workspaceRoot, "workspaceRoot");
|
|
13
|
+
const repoRoot = resolveOptionalExistingDir(input.repoRoot, "repoRoot");
|
|
14
|
+
const includeWorkspace = input.includeWorkspace ?? Boolean(workspaceRoot);
|
|
15
|
+
const includeRepoPatch = input.includeRepoPatch ?? Boolean(repoRoot);
|
|
16
|
+
const capsuleId = cleanString(input.capsuleId);
|
|
17
|
+
const capsule = capsuleId ? await getHostCapsule({ outputDir, capsulesFile: input.capsulesFile, id: capsuleId }) ?? undefined : undefined;
|
|
18
|
+
if (capsuleId && !capsule)
|
|
19
|
+
throw new Error(`host capsule not found: ${capsuleId}`);
|
|
20
|
+
const workspaceFileCount = includeWorkspace && workspaceRoot ? countFiles(workspaceRoot) : 0;
|
|
21
|
+
const repoDirty = includeRepoPatch && repoRoot ? gitStatus(repoRoot).trim().length > 0 : false;
|
|
22
|
+
const files = [];
|
|
23
|
+
if (includeWorkspace && workspaceRoot && workspaceFileCount > 0)
|
|
24
|
+
files.push(`${basename(workspaceRoot) || "workspace"}/`);
|
|
25
|
+
if (includeRepoPatch && repoRoot && repoDirty) {
|
|
26
|
+
files.push("repo-status.txt");
|
|
27
|
+
if (gitDiff(repoRoot, false).trim())
|
|
28
|
+
files.push("repo.patch");
|
|
29
|
+
if (gitDiff(repoRoot, true).trim())
|
|
30
|
+
files.push("repo-staged.patch");
|
|
31
|
+
}
|
|
32
|
+
if (capsule)
|
|
33
|
+
files.push("capsule.json");
|
|
34
|
+
files.push("meta.json");
|
|
35
|
+
const plan = {
|
|
36
|
+
runId,
|
|
37
|
+
outputDir,
|
|
38
|
+
exportDir,
|
|
39
|
+
workspaceRoot,
|
|
40
|
+
repoRoot,
|
|
41
|
+
includeWorkspace,
|
|
42
|
+
includeRepoPatch,
|
|
43
|
+
capsuleId,
|
|
44
|
+
capsule,
|
|
45
|
+
workspaceFileCount,
|
|
46
|
+
repoDirty,
|
|
47
|
+
files,
|
|
48
|
+
summary: ""
|
|
49
|
+
};
|
|
50
|
+
plan.summary = formatHostExportPlan(plan);
|
|
51
|
+
return plan;
|
|
52
|
+
}
|
|
53
|
+
export async function exportHostArtifacts(input = {}) {
|
|
54
|
+
const plan = await planHostExport(input);
|
|
55
|
+
const writtenFiles = [];
|
|
56
|
+
await rm(plan.exportDir, { recursive: true, force: true });
|
|
57
|
+
await mkdir(plan.exportDir, { recursive: true });
|
|
58
|
+
if (plan.includeWorkspace && plan.workspaceRoot && plan.workspaceFileCount > 0) {
|
|
59
|
+
const target = join(plan.exportDir, basename(plan.workspaceRoot) || "workspace");
|
|
60
|
+
await cp(plan.workspaceRoot, target, {
|
|
61
|
+
recursive: true,
|
|
62
|
+
force: true,
|
|
63
|
+
dereference: false,
|
|
64
|
+
filter: (source) => !shouldSkipExportPath(source)
|
|
65
|
+
});
|
|
66
|
+
writtenFiles.push(target);
|
|
67
|
+
}
|
|
68
|
+
if (plan.includeRepoPatch && plan.repoRoot && plan.repoDirty) {
|
|
69
|
+
const status = gitStatus(plan.repoRoot);
|
|
70
|
+
await writeFile(join(plan.exportDir, "repo-status.txt"), status, "utf8");
|
|
71
|
+
writtenFiles.push(join(plan.exportDir, "repo-status.txt"));
|
|
72
|
+
const diff = gitDiff(plan.repoRoot, false);
|
|
73
|
+
if (diff.trim()) {
|
|
74
|
+
await writeFile(join(plan.exportDir, "repo.patch"), diff, "utf8");
|
|
75
|
+
writtenFiles.push(join(plan.exportDir, "repo.patch"));
|
|
76
|
+
}
|
|
77
|
+
const staged = gitDiff(plan.repoRoot, true);
|
|
78
|
+
if (staged.trim()) {
|
|
79
|
+
await writeFile(join(plan.exportDir, "repo-staged.patch"), staged, "utf8");
|
|
80
|
+
writtenFiles.push(join(plan.exportDir, "repo-staged.patch"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (plan.capsule) {
|
|
84
|
+
const capsulePath = join(plan.exportDir, "capsule.json");
|
|
85
|
+
await writeFile(capsulePath, `${JSON.stringify(plan.capsule, null, 2)}\n`, "utf8");
|
|
86
|
+
writtenFiles.push(capsulePath);
|
|
87
|
+
}
|
|
88
|
+
const metaPath = join(plan.exportDir, "meta.json");
|
|
89
|
+
writtenFiles.push(metaPath);
|
|
90
|
+
const meta = createHostExportMeta(plan, writtenFiles);
|
|
91
|
+
await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
92
|
+
return {
|
|
93
|
+
...plan,
|
|
94
|
+
writtenFiles
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function formatHostExportPlan(plan) {
|
|
98
|
+
const lines = [
|
|
99
|
+
`host export: ${plan.runId}`,
|
|
100
|
+
`output: ${plan.exportDir}`,
|
|
101
|
+
`workspace: ${plan.workspaceRoot ?? "(none)"} files=${plan.workspaceFileCount}`,
|
|
102
|
+
`repo: ${plan.repoRoot ?? "(none)"} dirty=${plan.repoDirty ? "yes" : "no"}`,
|
|
103
|
+
`capsule: ${plan.capsule?.id ?? "(none)"}`
|
|
104
|
+
];
|
|
105
|
+
if (plan.files.length) {
|
|
106
|
+
lines.push("planned files:");
|
|
107
|
+
for (const file of plan.files)
|
|
108
|
+
lines.push(` - ${file}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
lines.push("planned files: (none)");
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
export function createHostExportMeta(plan, writtenFiles, now = new Date()) {
|
|
116
|
+
return {
|
|
117
|
+
schema: "king-ai.host-export.v1",
|
|
118
|
+
runId: plan.runId,
|
|
119
|
+
exportedAt: now.toISOString(),
|
|
120
|
+
outputDir: plan.outputDir,
|
|
121
|
+
exportDir: plan.exportDir,
|
|
122
|
+
workspaceRoot: plan.workspaceRoot,
|
|
123
|
+
repoRoot: plan.repoRoot,
|
|
124
|
+
includeWorkspace: plan.includeWorkspace,
|
|
125
|
+
includeRepoPatch: plan.includeRepoPatch,
|
|
126
|
+
capsuleId: plan.capsuleId,
|
|
127
|
+
capsule: plan.capsule,
|
|
128
|
+
workspaceFileCount: plan.workspaceFileCount,
|
|
129
|
+
repoDirty: plan.repoDirty,
|
|
130
|
+
files: plan.files,
|
|
131
|
+
writtenFiles
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function cleanString(value) {
|
|
135
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
136
|
+
}
|
|
137
|
+
function safeFilenameSegment(value, label) {
|
|
138
|
+
if (value === "." || value === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
|
|
139
|
+
throw new Error(`${label} must be a safe filename segment`);
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function buildExportRunId() {
|
|
144
|
+
return `export-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
145
|
+
}
|
|
146
|
+
function resolveOptionalExistingDir(value, label) {
|
|
147
|
+
const cleaned = cleanString(value);
|
|
148
|
+
if (!cleaned)
|
|
149
|
+
return undefined;
|
|
150
|
+
const resolved = resolve(cleaned);
|
|
151
|
+
if (!existsSync(resolved))
|
|
152
|
+
throw new Error(`${label} does not exist: ${resolved}`);
|
|
153
|
+
if (!statSync(resolved).isDirectory())
|
|
154
|
+
throw new Error(`${label} is not a directory: ${resolved}`);
|
|
155
|
+
if (label === "repoRoot" && !isGitRepo(resolved))
|
|
156
|
+
throw new Error(`${label} is not a git repository: ${resolved}`);
|
|
157
|
+
return resolved;
|
|
158
|
+
}
|
|
159
|
+
function shouldSkipExportPath(path) {
|
|
160
|
+
const normalized = path.replace(/\\/g, "/");
|
|
161
|
+
return /(^|\/)(\.git|node_modules|\.DS_Store)$/.test(normalized);
|
|
162
|
+
}
|
|
163
|
+
function countFiles(dir) {
|
|
164
|
+
let count = 0;
|
|
165
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
166
|
+
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === ".DS_Store")
|
|
167
|
+
continue;
|
|
168
|
+
const full = join(dir, entry.name);
|
|
169
|
+
if (entry.isDirectory())
|
|
170
|
+
count += countFiles(full);
|
|
171
|
+
else if (entry.isFile())
|
|
172
|
+
count += 1;
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
function gitStatus(repoRoot) {
|
|
177
|
+
return execFileSync("git", ["-C", repoRoot, "status", "--short"], {
|
|
178
|
+
encoding: "utf8",
|
|
179
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function gitDiff(repoRoot, staged) {
|
|
183
|
+
return execFileSync("git", ["-C", repoRoot, "diff", "--binary", ...(staged ? ["--cached"] : [])], {
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface HostFeedbackPathInput {
|
|
2
|
+
outputDir?: string;
|
|
3
|
+
feedbackFile?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HostFeedbackRecordInput extends HostFeedbackPathInput {
|
|
6
|
+
runId?: string;
|
|
7
|
+
agent?: string;
|
|
8
|
+
taskId?: string;
|
|
9
|
+
capsuleId?: string;
|
|
10
|
+
status?: "completed" | "failed" | "partial" | "cancelled";
|
|
11
|
+
rating?: number;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
inputTokens?: number;
|
|
14
|
+
outputTokens?: number;
|
|
15
|
+
totalTokens?: number;
|
|
16
|
+
skill?: string;
|
|
17
|
+
taskCompleted?: boolean;
|
|
18
|
+
errored?: boolean;
|
|
19
|
+
humanIntervention?: boolean;
|
|
20
|
+
note?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HostFeedbackListInput extends HostFeedbackPathInput {
|
|
23
|
+
runId?: string;
|
|
24
|
+
agent?: string;
|
|
25
|
+
skill?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface HostFeedbackRecord {
|
|
29
|
+
schema: "king-ai.host-feedback.v1";
|
|
30
|
+
createdAt: string;
|
|
31
|
+
runId?: string;
|
|
32
|
+
agent?: string;
|
|
33
|
+
taskId?: string;
|
|
34
|
+
capsuleId?: string;
|
|
35
|
+
status: "completed" | "failed" | "partial" | "cancelled";
|
|
36
|
+
rating?: number;
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
skill?: string;
|
|
42
|
+
taskCompleted: boolean;
|
|
43
|
+
errored: boolean;
|
|
44
|
+
humanIntervention: boolean;
|
|
45
|
+
note?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface HostFeedbackSummary {
|
|
48
|
+
records: number;
|
|
49
|
+
completed: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
partial: number;
|
|
52
|
+
cancelled: number;
|
|
53
|
+
taskCompleted: number;
|
|
54
|
+
errored: number;
|
|
55
|
+
humanIntervention: number;
|
|
56
|
+
totalDurationMs: number;
|
|
57
|
+
totalTokens: number;
|
|
58
|
+
byAgent: Array<{
|
|
59
|
+
agent: string;
|
|
60
|
+
records: number;
|
|
61
|
+
completed: number;
|
|
62
|
+
failed: number;
|
|
63
|
+
totalTokens: number;
|
|
64
|
+
}>;
|
|
65
|
+
bySkill: Array<{
|
|
66
|
+
skill: string;
|
|
67
|
+
records: number;
|
|
68
|
+
completed: number;
|
|
69
|
+
failed: number;
|
|
70
|
+
totalTokens: number;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
export declare function hostFeedbackPathForOutputDir(outputDir?: string): string;
|
|
74
|
+
export declare function recordHostFeedback(input: HostFeedbackRecordInput, now?: () => Date): Promise<HostFeedbackRecord>;
|
|
75
|
+
export declare function listHostFeedback(input?: HostFeedbackListInput): Promise<HostFeedbackRecord[]>;
|
|
76
|
+
export declare function summarizeHostFeedback(input?: HostFeedbackListInput): Promise<HostFeedbackSummary>;
|
|
77
|
+
export declare function formatHostFeedback(records: HostFeedbackRecord[]): string;
|
|
78
|
+
export declare function formatHostFeedbackSummary(summary: HostFeedbackSummary): string;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { appendJsonl, readJsonl } from "./jsonl.js";
|
|
3
|
+
export function hostFeedbackPathForOutputDir(outputDir = "deliverables") {
|
|
4
|
+
return join(resolve(outputDir), "run-feedback.jsonl");
|
|
5
|
+
}
|
|
6
|
+
export async function recordHostFeedback(input, now = () => new Date()) {
|
|
7
|
+
const inputTokens = nonNegativeInteger(input.inputTokens, "inputTokens") ?? 0;
|
|
8
|
+
const outputTokens = nonNegativeInteger(input.outputTokens, "outputTokens") ?? 0;
|
|
9
|
+
const totalTokens = nonNegativeInteger(input.totalTokens, "totalTokens") ?? inputTokens + outputTokens;
|
|
10
|
+
const status = normalizeStatus(input.status ?? (input.errored ? "failed" : "completed"));
|
|
11
|
+
const record = {
|
|
12
|
+
schema: "king-ai.host-feedback.v1",
|
|
13
|
+
createdAt: now().toISOString(),
|
|
14
|
+
runId: cleanString(input.runId),
|
|
15
|
+
agent: cleanString(input.agent),
|
|
16
|
+
taskId: cleanString(input.taskId),
|
|
17
|
+
capsuleId: cleanString(input.capsuleId),
|
|
18
|
+
status,
|
|
19
|
+
rating: normalizeRating(input.rating),
|
|
20
|
+
durationMs: nonNegativeInteger(input.durationMs, "durationMs"),
|
|
21
|
+
inputTokens,
|
|
22
|
+
outputTokens,
|
|
23
|
+
totalTokens,
|
|
24
|
+
skill: cleanString(input.skill),
|
|
25
|
+
taskCompleted: input.taskCompleted ?? status === "completed",
|
|
26
|
+
errored: input.errored ?? status === "failed",
|
|
27
|
+
humanIntervention: input.humanIntervention ?? false,
|
|
28
|
+
note: cleanString(input.note)
|
|
29
|
+
};
|
|
30
|
+
const cleaned = dropUndefined(record);
|
|
31
|
+
await appendJsonl(feedbackPath(input), cleaned);
|
|
32
|
+
return cleaned;
|
|
33
|
+
}
|
|
34
|
+
export async function listHostFeedback(input = {}) {
|
|
35
|
+
const runId = cleanString(input.runId);
|
|
36
|
+
const agent = cleanString(input.agent);
|
|
37
|
+
const skill = cleanString(input.skill);
|
|
38
|
+
const records = (await readJsonl(feedbackPath(input))).filter(isFeedbackRecord).filter((record) => {
|
|
39
|
+
if (runId && record.runId !== runId)
|
|
40
|
+
return false;
|
|
41
|
+
if (agent && record.agent !== agent)
|
|
42
|
+
return false;
|
|
43
|
+
if (skill && record.skill !== skill)
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
const limit = normalizeLimit(input.limit);
|
|
48
|
+
return limit ? records.slice(-limit).reverse() : records;
|
|
49
|
+
}
|
|
50
|
+
export async function summarizeHostFeedback(input = {}) {
|
|
51
|
+
return summarizeFeedback(await listHostFeedback(input));
|
|
52
|
+
}
|
|
53
|
+
export function formatHostFeedback(records) {
|
|
54
|
+
if (records.length === 0)
|
|
55
|
+
return "no host feedback";
|
|
56
|
+
return records.map((record) => {
|
|
57
|
+
const subject = [record.runId, record.taskId, record.capsuleId].filter(Boolean).join(" ") || "(no subject)";
|
|
58
|
+
return `${record.createdAt} ${record.status}${record.agent ? ` @${record.agent}` : ""} ${subject} tokens=${record.totalTokens}${record.note ? ` ${record.note}` : ""}`;
|
|
59
|
+
}).join("\n");
|
|
60
|
+
}
|
|
61
|
+
export function formatHostFeedbackSummary(summary) {
|
|
62
|
+
const lines = [
|
|
63
|
+
`feedback: records=${summary.records} completed=${summary.completed} failed=${summary.failed} partial=${summary.partial} cancelled=${summary.cancelled}`,
|
|
64
|
+
`tasks: completed=${summary.taskCompleted} errored=${summary.errored} humanIntervention=${summary.humanIntervention}`,
|
|
65
|
+
`usage: durationMs=${summary.totalDurationMs} tokens=${summary.totalTokens}`
|
|
66
|
+
];
|
|
67
|
+
if (summary.byAgent.length) {
|
|
68
|
+
lines.push("by agent:");
|
|
69
|
+
for (const entry of summary.byAgent)
|
|
70
|
+
lines.push(` - ${entry.agent}: records=${entry.records} completed=${entry.completed} failed=${entry.failed} tokens=${entry.totalTokens}`);
|
|
71
|
+
}
|
|
72
|
+
if (summary.bySkill.length) {
|
|
73
|
+
lines.push("by skill:");
|
|
74
|
+
for (const entry of summary.bySkill)
|
|
75
|
+
lines.push(` - ${entry.skill}: records=${entry.records} completed=${entry.completed} failed=${entry.failed} tokens=${entry.totalTokens}`);
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
function summarizeFeedback(records) {
|
|
80
|
+
const byAgent = new Map();
|
|
81
|
+
const bySkill = new Map();
|
|
82
|
+
const summary = {
|
|
83
|
+
records: records.length,
|
|
84
|
+
completed: 0,
|
|
85
|
+
failed: 0,
|
|
86
|
+
partial: 0,
|
|
87
|
+
cancelled: 0,
|
|
88
|
+
taskCompleted: 0,
|
|
89
|
+
errored: 0,
|
|
90
|
+
humanIntervention: 0,
|
|
91
|
+
totalDurationMs: 0,
|
|
92
|
+
totalTokens: 0,
|
|
93
|
+
byAgent: [],
|
|
94
|
+
bySkill: []
|
|
95
|
+
};
|
|
96
|
+
for (const record of records) {
|
|
97
|
+
summary[record.status] += 1;
|
|
98
|
+
if (record.taskCompleted)
|
|
99
|
+
summary.taskCompleted += 1;
|
|
100
|
+
if (record.errored)
|
|
101
|
+
summary.errored += 1;
|
|
102
|
+
if (record.humanIntervention)
|
|
103
|
+
summary.humanIntervention += 1;
|
|
104
|
+
summary.totalDurationMs += record.durationMs ?? 0;
|
|
105
|
+
summary.totalTokens += record.totalTokens;
|
|
106
|
+
if (record.agent)
|
|
107
|
+
addGroup(byAgent, record.agent, record);
|
|
108
|
+
if (record.skill)
|
|
109
|
+
addSkillGroup(bySkill, record.skill, record);
|
|
110
|
+
}
|
|
111
|
+
summary.byAgent = [...byAgent.values()];
|
|
112
|
+
summary.bySkill = [...bySkill.values()];
|
|
113
|
+
return summary;
|
|
114
|
+
}
|
|
115
|
+
function addGroup(groups, agent, record) {
|
|
116
|
+
const entry = groups.get(agent) ?? { agent, records: 0, completed: 0, failed: 0, totalTokens: 0 };
|
|
117
|
+
entry.records += 1;
|
|
118
|
+
if (record.status === "completed")
|
|
119
|
+
entry.completed += 1;
|
|
120
|
+
if (record.status === "failed")
|
|
121
|
+
entry.failed += 1;
|
|
122
|
+
entry.totalTokens += record.totalTokens;
|
|
123
|
+
groups.set(agent, entry);
|
|
124
|
+
}
|
|
125
|
+
function addSkillGroup(groups, skill, record) {
|
|
126
|
+
const entry = groups.get(skill) ?? { skill, records: 0, completed: 0, failed: 0, totalTokens: 0 };
|
|
127
|
+
entry.records += 1;
|
|
128
|
+
if (record.status === "completed")
|
|
129
|
+
entry.completed += 1;
|
|
130
|
+
if (record.status === "failed")
|
|
131
|
+
entry.failed += 1;
|
|
132
|
+
entry.totalTokens += record.totalTokens;
|
|
133
|
+
groups.set(skill, entry);
|
|
134
|
+
}
|
|
135
|
+
function feedbackPath(input) {
|
|
136
|
+
return input.feedbackFile ? resolve(input.feedbackFile) : hostFeedbackPathForOutputDir(input.outputDir);
|
|
137
|
+
}
|
|
138
|
+
function isFeedbackRecord(value) {
|
|
139
|
+
if (!value || typeof value !== "object")
|
|
140
|
+
return false;
|
|
141
|
+
const record = value;
|
|
142
|
+
return record.schema === "king-ai.host-feedback.v1" && typeof record.createdAt === "string" && typeof record.totalTokens === "number";
|
|
143
|
+
}
|
|
144
|
+
function normalizeStatus(value) {
|
|
145
|
+
if (value === "completed" || value === "failed" || value === "partial" || value === "cancelled")
|
|
146
|
+
return value;
|
|
147
|
+
throw new Error(`invalid feedback status: ${String(value)}`);
|
|
148
|
+
}
|
|
149
|
+
function normalizeRating(value) {
|
|
150
|
+
if (value === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
153
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 5)
|
|
154
|
+
throw new Error("rating must be between 1 and 5");
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
function nonNegativeInteger(value, label) {
|
|
158
|
+
if (value === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
161
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
162
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
163
|
+
return Math.floor(parsed);
|
|
164
|
+
}
|
|
165
|
+
function normalizeLimit(value) {
|
|
166
|
+
if (value === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
169
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
170
|
+
throw new Error("limit must be a positive integer");
|
|
171
|
+
return Math.floor(parsed);
|
|
172
|
+
}
|
|
173
|
+
function cleanString(value) {
|
|
174
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
175
|
+
}
|
|
176
|
+
function dropUndefined(value) {
|
|
177
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
178
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface HostHomeEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
source: string;
|
|
4
|
+
target: string;
|
|
5
|
+
linked: boolean;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveHostHomeEntryNames(env?: NodeJS.ProcessEnv): string[];
|
|
9
|
+
export declare function resolveHostHomeEntry(entry: string, home?: string): {
|
|
10
|
+
name: string;
|
|
11
|
+
source: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function linkHostHomeEntries(agentHome: string, env?: NodeJS.ProcessEnv, home?: string): Promise<HostHomeEntry[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { mkdir, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, delimiter, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
function splitEntries(value) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return [];
|
|
8
|
+
return value
|
|
9
|
+
.split(delimiter)
|
|
10
|
+
.flatMap((part) => part.split(","))
|
|
11
|
+
.map((part) => part.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.filter((entry, idx, all) => all.indexOf(entry) === idx);
|
|
14
|
+
}
|
|
15
|
+
export function resolveHostHomeEntryNames(env = process.env) {
|
|
16
|
+
return splitEntries(env.KING_AI_HOST_HOME_ENTRIES);
|
|
17
|
+
}
|
|
18
|
+
export function resolveHostHomeEntry(entry, home = homedir()) {
|
|
19
|
+
if (entry.includes(".."))
|
|
20
|
+
return null;
|
|
21
|
+
const expanded = entry.replace(/^~(?=$|\/|\\)/, home);
|
|
22
|
+
const source = isAbsolute(expanded) ? resolve(expanded) : resolve(home, expanded);
|
|
23
|
+
const rel = relative(home, source);
|
|
24
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel))
|
|
25
|
+
return null;
|
|
26
|
+
const parts = rel.split(/[\\/]/);
|
|
27
|
+
if (parts.length !== 1)
|
|
28
|
+
return null;
|
|
29
|
+
const name = basename(source);
|
|
30
|
+
if (!name.startsWith("."))
|
|
31
|
+
return null;
|
|
32
|
+
return { name, source };
|
|
33
|
+
}
|
|
34
|
+
export async function linkHostHomeEntries(agentHome, env = process.env, home = homedir()) {
|
|
35
|
+
const entries = [];
|
|
36
|
+
await mkdir(agentHome, { recursive: true });
|
|
37
|
+
for (const raw of resolveHostHomeEntryNames(env)) {
|
|
38
|
+
const resolved = resolveHostHomeEntry(raw, home);
|
|
39
|
+
if (!resolved) {
|
|
40
|
+
entries.push({ name: raw, source: raw, target: "", linked: false, reason: "entry must be a single host-home dotfile or dot directory" });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const target = join(agentHome, resolved.name);
|
|
44
|
+
if (!existsSync(resolved.source)) {
|
|
45
|
+
entries.push({ ...resolved, target, linked: false, reason: "source does not exist" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const stat = lstatSync(resolved.source);
|
|
49
|
+
await rm(target, { recursive: true, force: true });
|
|
50
|
+
await symlink(resolved.source, target, stat.isDirectory() ? "dir" : "file");
|
|
51
|
+
entries.push({ ...resolved, target, linked: true });
|
|
52
|
+
}
|
|
53
|
+
return entries;
|
|
54
|
+
}
|