@sagentlab/navarch-runtime 0.1.91 → 0.1.92
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 +3 -0
- package/dist/adapters/claude-conversation.cjs +269 -0
- package/dist/adapters/claude.cjs +2 -0
- package/dist/api.cjs +14 -0
- package/dist/claim-loop.cjs +19 -7
- package/dist/cli.cjs +15 -0
- package/dist/config.cjs +5 -0
- package/dist/conversation/capability.cjs +53 -0
- package/dist/conversation/journal.cjs +153 -0
- package/dist/conversation/loop.cjs +232 -0
- package/dist/conversation/prompt.cjs +53 -0
- package/dist/conversation/turn-runner.cjs +402 -0
- package/dist/conversation/workspace.cjs +103 -0
- package/dist/prompt.cjs +35 -0
- package/dist/session.cjs +15 -5
- package/dist/update-coordinator.cjs +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.conversationEnv = conversationEnv;
|
|
7
|
+
exports.validRepositoryUrl = validRepositoryUrl;
|
|
8
|
+
exports.conversationRoot = conversationRoot;
|
|
9
|
+
exports.prepareConversationWorkspace = prepareConversationWorkspace;
|
|
10
|
+
exports.removeConversationWorkspace = removeConversationWorkspace;
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const sandbox_cjs_1 = require("../sandbox.cjs");
|
|
14
|
+
const journal_cjs_1 = require("./journal.cjs");
|
|
15
|
+
const CLONE_TIMEOUT_MS = 3 * 60_000;
|
|
16
|
+
const REPOSITORY_MARKER = "repository-url";
|
|
17
|
+
/** Variables that carry this runtime's own or a task's credentials, never a conversation's. */
|
|
18
|
+
const WITHHELD_ENV = /^(NAVARCH_|GH_|GITHUB_|GIT_ASKPASS$|SSH_ASKPASS$|SSH_AUTH_SOCK$)/;
|
|
19
|
+
/**
|
|
20
|
+
* Environment for a conversation child. It inherits the machine's own login
|
|
21
|
+
* (HOME, PATH, provider keys the local agent needs) but not the Navarch
|
|
22
|
+
* machine token, broker or GitHub credentials the runtime holds for tasks.
|
|
23
|
+
*/
|
|
24
|
+
function conversationEnv(base = process.env) {
|
|
25
|
+
const env = {};
|
|
26
|
+
for (const [key, value] of Object.entries(base)) {
|
|
27
|
+
if (value === undefined || WITHHELD_ENV.test(key))
|
|
28
|
+
continue;
|
|
29
|
+
env[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Maps the project's stored repository value to a GitHub https clone URL.
|
|
35
|
+
* The control plane stores a free-form value; the task path accepts
|
|
36
|
+
* `owner/name`, `git@github.com:owner/name(.git)` and `https://github.com/...`
|
|
37
|
+
* (lib/navarch/github-repository.ts), and so does this one. Anything else
|
|
38
|
+
* (another host, `ext::`, `file:`, `-` options, embedded credentials) returns
|
|
39
|
+
* null, so the runtime never hands an unvalidated string to git.
|
|
40
|
+
*/
|
|
41
|
+
function validRepositoryUrl(value) {
|
|
42
|
+
if (typeof value !== "string" || value.length > 2048 || /[\s\0]/.test(value))
|
|
43
|
+
return null;
|
|
44
|
+
const fullName = value
|
|
45
|
+
.replace(/^git@github\.com:/i, "")
|
|
46
|
+
.replace(/^https:\/\/github\.com\//i, "")
|
|
47
|
+
.replace(/\.git$/i, "")
|
|
48
|
+
.replace(/\/+$/, "");
|
|
49
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(fullName) || fullName.startsWith("-"))
|
|
50
|
+
return null;
|
|
51
|
+
return `https://github.com/${fullName}.git`;
|
|
52
|
+
}
|
|
53
|
+
function conversationRoot(workspaceRoot, conversationId) {
|
|
54
|
+
return node_path_1.default.join(workspaceRoot, "conversations", (0, journal_cjs_1.assertUuid)(conversationId, "conversation id"));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Prepares the conversation's own checkout. Each conversation gets a separate
|
|
58
|
+
* directory, so two conversations never share files, history or git config.
|
|
59
|
+
* The first turn clones; later turns of the same conversation reuse the
|
|
60
|
+
* checkout so a follow-up sees the same revision as the first answer.
|
|
61
|
+
*
|
|
62
|
+
* The clone uses the machine's own git credentials (the same ones a local
|
|
63
|
+
* `git clone` would use), never a Navarch server-side provider credential.
|
|
64
|
+
* A repository this machine cannot read fails the turn instead of falling back.
|
|
65
|
+
*/
|
|
66
|
+
async function prepareConversationWorkspace(options) {
|
|
67
|
+
const url = validRepositoryUrl(options.repositoryUrl);
|
|
68
|
+
if (!url)
|
|
69
|
+
throw new Error("repository URL is not a plain https URL");
|
|
70
|
+
const runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
71
|
+
const root = conversationRoot(options.workspaceRoot, options.conversationId);
|
|
72
|
+
const workspace = {
|
|
73
|
+
root,
|
|
74
|
+
checkout: node_path_1.default.join(root, "repo"),
|
|
75
|
+
metaDir: node_path_1.default.join(root, "meta"),
|
|
76
|
+
};
|
|
77
|
+
await node_fs_1.promises.mkdir(workspace.metaDir, { recursive: true, mode: 0o700 });
|
|
78
|
+
const marker = node_path_1.default.join(root, REPOSITORY_MARKER);
|
|
79
|
+
const recorded = await node_fs_1.promises.readFile(marker, "utf8").catch(() => null);
|
|
80
|
+
const hasCheckout = await node_fs_1.promises.stat(node_path_1.default.join(workspace.checkout, ".git")).then(() => true, () => false);
|
|
81
|
+
if (hasCheckout && recorded === url)
|
|
82
|
+
return workspace;
|
|
83
|
+
// New conversation, a changed repository URL, or an interrupted clone.
|
|
84
|
+
await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true });
|
|
85
|
+
const env = {
|
|
86
|
+
...options.env,
|
|
87
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
88
|
+
GIT_ALLOW_PROTOCOL: "https",
|
|
89
|
+
GIT_CONFIG_COUNT: "1",
|
|
90
|
+
GIT_CONFIG_KEY_0: "core.hooksPath",
|
|
91
|
+
GIT_CONFIG_VALUE_0: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
92
|
+
};
|
|
93
|
+
const result = await runner.run("git", ["clone", "--quiet", "--filter=blob:none", "--no-tags", "--", url, workspace.checkout], { cwd: root, env, timeoutMs: CLONE_TIMEOUT_MS, signal: options.signal });
|
|
94
|
+
if (result.code !== 0) {
|
|
95
|
+
await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true }).catch(() => undefined);
|
|
96
|
+
throw new Error(`git clone failed with exit ${result.code}: ${result.stderr.slice(-500)}`);
|
|
97
|
+
}
|
|
98
|
+
await node_fs_1.promises.writeFile(marker, url, { encoding: "utf8", mode: 0o600 });
|
|
99
|
+
return workspace;
|
|
100
|
+
}
|
|
101
|
+
async function removeConversationWorkspace(workspaceRoot, conversationId) {
|
|
102
|
+
await node_fs_1.promises.rm(conversationRoot(workspaceRoot, conversationId), { recursive: true, force: true });
|
|
103
|
+
}
|
package/dist/prompt.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderPrompt = renderPrompt;
|
|
4
4
|
exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
|
|
5
|
+
exports.renderCompletionRemediationPrompt = renderCompletionRemediationPrompt;
|
|
5
6
|
exports.renderHostedCheckpointNote = renderHostedCheckpointNote;
|
|
6
7
|
const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
|
|
7
8
|
const PROCESS_GUIDANCE = "Process management: this host may run other agent sessions. Keep the PID or tool handle of each preview server you start and stop only that owned process. Never use broad process-name or command-line matching (pkill, killall) to clean up servers: the agent launcher carries the task prompt in its arguments and can match those patterns, terminating your own session. Use a different port when a port is occupied by a process you do not own. Keep scratch files in the current worktree; paths in previous transcripts belong to earlier sessions.";
|
|
@@ -74,6 +75,40 @@ function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
|
74
75
|
"",
|
|
75
76
|
].join("\n");
|
|
76
77
|
}
|
|
78
|
+
/** Keeps a long previous report from crowding the brief out of the prompt. */
|
|
79
|
+
const REMEDIATION_REPORT_LIMIT = 8_000;
|
|
80
|
+
/**
|
|
81
|
+
* Builds the turn that follows a remediable completion rejection (for example
|
|
82
|
+
* `verification_verdict_invalid` or `pr_required`). Each turn is a fresh agent
|
|
83
|
+
* invocation with no memory of the last one, so the rejection alone gave the
|
|
84
|
+
* agent nothing to act on: a verifier answered BLOCKED because it had "no
|
|
85
|
+
* acceptance criteria or task to verify". The turn therefore carries the full
|
|
86
|
+
* task brief, the rejection, and the previous turn's report.
|
|
87
|
+
*/
|
|
88
|
+
function renderCompletionRemediationPrompt(args) {
|
|
89
|
+
const report = args.previousReport.trim();
|
|
90
|
+
const clipped = report.length > REMEDIATION_REPORT_LIMIT
|
|
91
|
+
? `${report.slice(0, REMEDIATION_REPORT_LIMIT)}\n\n[... truncated]`
|
|
92
|
+
: report;
|
|
93
|
+
return [
|
|
94
|
+
args.originalPrompt.trim(),
|
|
95
|
+
"",
|
|
96
|
+
"## Completion was rejected",
|
|
97
|
+
"",
|
|
98
|
+
"Your previous turn on this task finished, but the Navarch control plane rejected its completion:",
|
|
99
|
+
"",
|
|
100
|
+
...args.rejection.trim().split("\n").map((line) => `> ${line}`),
|
|
101
|
+
"",
|
|
102
|
+
`This is correction turn ${args.attempt} of ${args.maxAttempts}. The task above is unchanged, and you are in the`,
|
|
103
|
+
"same worktree, so earlier edits, commits, and branches are still here. Reuse the work and findings from",
|
|
104
|
+
"the previous turn instead of starting over, fix what the rejection names, and finish the task.",
|
|
105
|
+
"Your final report replaces the previous one, so restate it in full in the required format.",
|
|
106
|
+
...(clipped
|
|
107
|
+
? ["", "### Previous turn's final report", "", clipped]
|
|
108
|
+
: []),
|
|
109
|
+
"",
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
77
112
|
/**
|
|
78
113
|
* Tells the agent its worktree starts from a hosted attempt's checkpoint
|
|
79
114
|
* (GitWorktree.restoredHostedCheckpoint), mirroring the hosted side's own
|
package/dist/session.cjs
CHANGED
|
@@ -523,10 +523,12 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
523
523
|
activeAbortController = new AbortController();
|
|
524
524
|
if (leaseLost || deps.shutdownSignal?.aborted)
|
|
525
525
|
activeAbortController.abort();
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
// The task brief plus any mid-flight guidance. Correction turns build on
|
|
527
|
+
// this rather than on the previous turn's prompt so they never nest.
|
|
528
|
+
const briefPrompt = deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
529
|
+
? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
|
|
530
|
+
: promptText;
|
|
531
|
+
const runPrompt = nextPrompt ?? briefPrompt;
|
|
530
532
|
nextPrompt = null;
|
|
531
533
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
532
534
|
adapterStarted = true;
|
|
@@ -780,7 +782,15 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
780
782
|
if (remediable && completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
|
|
781
783
|
completionRemediationRetries += 1;
|
|
782
784
|
log.warn(`completion for ${leaseId} needs more work; restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
783
|
-
|
|
785
|
+
// A new turn is a fresh agent invocation: the rejection alone would
|
|
786
|
+
// reach an agent that has never seen the task.
|
|
787
|
+
nextPrompt = (0, prompt_cjs_1.renderCompletionRemediationPrompt)({
|
|
788
|
+
originalPrompt: briefPrompt,
|
|
789
|
+
rejection: redactedRejection,
|
|
790
|
+
previousReport: redactedReport,
|
|
791
|
+
attempt: completionRemediationRetries,
|
|
792
|
+
maxAttempts: COMPLETION_REMEDIATION_RETRIES,
|
|
793
|
+
});
|
|
784
794
|
continue;
|
|
785
795
|
}
|
|
786
796
|
if (remediable) {
|
|
@@ -53,11 +53,12 @@ class RuntimeUpdateCoordinator {
|
|
|
53
53
|
void this.applyRestart(directive);
|
|
54
54
|
}
|
|
55
55
|
async applyRestart(directive) {
|
|
56
|
-
const { heartbeat, claimLoop, capacity } = this.options;
|
|
56
|
+
const { heartbeat, claimLoop, conversationLoop, capacity } = this.options;
|
|
57
57
|
try {
|
|
58
58
|
heartbeat.setUpdateState("draining");
|
|
59
59
|
log.info(`remote restart ${directive.command_id} received; draining active sessions`);
|
|
60
60
|
await claimLoop.drain();
|
|
61
|
+
await conversationLoop?.drain();
|
|
61
62
|
await capacity.waitForIdle();
|
|
62
63
|
log.info(`remote restart ${directive.command_id} ready; handing off to supervisor`);
|
|
63
64
|
(this.options.requestRestart ?? ((code) => process.exit(code)))(supervisor_cjs_1.REMOTE_RESTART_EXIT_CODE);
|
|
@@ -67,6 +68,7 @@ class RuntimeUpdateCoordinator {
|
|
|
67
68
|
log.error(`remote restart failed: ${message}`);
|
|
68
69
|
heartbeat.setUpdateState("failed", message.slice(0, 500));
|
|
69
70
|
claimLoop.start();
|
|
71
|
+
conversationLoop?.start();
|
|
70
72
|
this.restartInProgress = null;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
@@ -74,7 +76,7 @@ class RuntimeUpdateCoordinator {
|
|
|
74
76
|
return (this.options.now ?? Date.now)();
|
|
75
77
|
}
|
|
76
78
|
async apply(directive) {
|
|
77
|
-
const { config, heartbeat, claimLoop, capacity } = this.options;
|
|
79
|
+
const { config, heartbeat, claimLoop, conversationLoop, capacity } = this.options;
|
|
78
80
|
try {
|
|
79
81
|
heartbeat.setUpdateState("staging");
|
|
80
82
|
log.info(`staging runtime ${directive.target_version}`);
|
|
@@ -82,6 +84,7 @@ class RuntimeUpdateCoordinator {
|
|
|
82
84
|
heartbeat.setUpdateState("draining");
|
|
83
85
|
log.info("runtime update staged; draining active sessions before restart");
|
|
84
86
|
await claimLoop.drain();
|
|
87
|
+
await conversationLoop?.drain();
|
|
85
88
|
await capacity.waitForIdle();
|
|
86
89
|
await (0, update_installer_cjs_1.writePendingUpdate)(config.configDir, staged, directive.rollout_id);
|
|
87
90
|
log.info(`runtime ${directive.target_version} ready; handing off to supervisor`);
|
|
@@ -96,6 +99,7 @@ class RuntimeUpdateCoordinator {
|
|
|
96
99
|
log.error(`runtime update failed: ${message} (retrying ${directive.target_version} in ${Math.round(delayMs / 1000)}s)`);
|
|
97
100
|
heartbeat.setUpdateState("failed", message.slice(0, 500));
|
|
98
101
|
claimLoop.start();
|
|
102
|
+
conversationLoop?.start();
|
|
99
103
|
this.targetInProgress = null;
|
|
100
104
|
}
|
|
101
105
|
}
|
package/package.json
CHANGED