@strayl/agent 0.1.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/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_10_1772719084959.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_1_1772715451195.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_2_1772715452775.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_3_1772715454851.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_4_1772715457128.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_5_1772715464496.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_6_1772715466914.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_7_1772717269500.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_8_1772717274176.json +1 -0
- package/.strayl/checkpoints/03bdaa7e-3d71-44ee-9ff1-e23947c145b4/cp_9_1772717277769.json +1 -0
- package/.strayl/checkpoints/3f487378-fdba-413d-8c85-71e219cf4029/cp_1_1772710881634.json +1 -0
- package/.strayl/checkpoints/3f487378-fdba-413d-8c85-71e219cf4029/cp_2_1772710883272.json +1 -0
- package/.strayl/checkpoints/4c4d6f62-9e95-4790-9642-dced72212054/cp_1_1772722301047.json +1 -0
- package/.strayl/checkpoints/60ad3c6e-7e08-4841-9244-db9ef1351f94/cp_1_1772723527023.json +1 -0
- package/.strayl/checkpoints/6144a383-958f-478e-9f08-b3e435671beb/cp_1_1772723741593.json +1 -0
- package/.strayl/checkpoints/64d547ea-9114-4eac-8066-8c1d1cfafce3/cp_1_1772722436077.json +1 -0
- package/.strayl/checkpoints/88adc272-3c4f-410d-971a-dccd3a5a7c55/cp_1_1772723725211.json +1 -0
- package/.strayl/checkpoints/995ba1b6-6a4c-41c5-8a24-59d8475e31d4/cp_1_1772715363842.json +1 -0
- package/.strayl/checkpoints/aa9e0d03-bebe-49fb-b20d-7b5e5a3f299c/cp_1_1772715381414.json +1 -0
- package/.strayl/checkpoints/b0c6c2a4-a34d-451a-8937-c4235b306b2e/cp_1_1772711029179.json +1 -0
- package/.strayl/checkpoints/b4123afa-4e61-4a6e-b629-ef82273fb9af/cp_1_1772721833257.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_10_1772715577535.tmp +0 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_1_1772715557995.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_2_1772715560232.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_3_1772715562207.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_4_1772715564077.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_5_1772715566552.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_6_1772715569518.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_7_1772715571538.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_8_1772715573416.json +1 -0
- package/.strayl/checkpoints/b7fcfea4-6d41-4b31-98bd-e442f5b48f98/cp_9_1772715575737.json +1 -0
- package/.strayl/checkpoints/d42651a8-3bb2-4456-8775-66088ed31aca/cp_2_1772711043652.json +1 -0
- package/.strayl/checkpoints/ea81c9e2-47f7-4446-865d-40cb369d7944/cp_1_1772722131331.json +1 -0
- package/.strayl/checkpoints/eaaa83aa-18e4-4e74-be3a-1b1113f58dbe/cp_1_1772715398883.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_10_1772721968386.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_1_1772721929655.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_2_1772721935883.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_3_1772721940170.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_4_1772721945158.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_5_1772721949901.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_6_1772721954449.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_7_1772721957297.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_8_1772721961731.json +1 -0
- package/.strayl/checkpoints/ebb33f2b-ec62-4dba-9d84-7ccc4a54a255/cp_9_1772721964921.json +1 -0
- package/.strayl/checkpoints/f94498fa-cf5b-48de-a1f9-4c4754695dce/cp_1_1772715412172.json +1 -0
- package/.strayl/checkpoints/f94498fa-cf5b-48de-a1f9-4c4754695dce/cp_2_1772715414557.json +1 -0
- package/.strayl/checkpoints/f94498fa-cf5b-48de-a1f9-4c4754695dce/cp_3_1772715416369.json +1 -0
- package/.strayl/checkpoints/f94498fa-cf5b-48de-a1f9-4c4754695dce/cp_4_1772715421585.json +1 -0
- package/.strayl/logs/bg_1772680728576.log +2 -0
- package/INTEGRATION-PLAN.md +385 -0
- package/build.ts +14 -0
- package/dist/agent.js +13876 -0
- package/hello.txt +4 -0
- package/package.json +23 -0
- package/run.sh +84 -0
- package/src/agent.ts +440 -0
- package/src/checkpoints/manager.ts +112 -0
- package/src/context/manager.ts +185 -0
- package/src/context/summarizer.ts +104 -0
- package/src/context/trim.ts +55 -0
- package/src/emitter.ts +14 -0
- package/src/hitl/manager.ts +77 -0
- package/src/hitl/transport.ts +13 -0
- package/src/index.ts +116 -0
- package/src/llm/client.ts +276 -0
- package/src/llm/gemini-native.ts +307 -0
- package/src/llm/models.ts +64 -0
- package/src/middleware/compose.ts +24 -0
- package/src/middleware/credential-scrubbing.ts +31 -0
- package/src/middleware/forbidden-packages.ts +107 -0
- package/src/middleware/plan-mode.ts +143 -0
- package/src/middleware/prompt-caching.ts +21 -0
- package/src/middleware/tool-compression.ts +25 -0
- package/src/middleware/tool-filter.ts +13 -0
- package/src/prompts/implementation-mode.md +16 -0
- package/src/prompts/plan-mode.md +51 -0
- package/src/prompts/system.ts +173 -0
- package/src/skills/loader.ts +53 -0
- package/src/stdin-listener.ts +62 -0
- package/src/subagents/definitions.ts +72 -0
- package/src/subagents/manager.ts +140 -0
- package/src/todos/manager.ts +61 -0
- package/src/tools/builtin/delete.ts +29 -0
- package/src/tools/builtin/edit.ts +74 -0
- package/src/tools/builtin/exec.ts +216 -0
- package/src/tools/builtin/glob.ts +104 -0
- package/src/tools/builtin/grep.ts +115 -0
- package/src/tools/builtin/ls.ts +54 -0
- package/src/tools/builtin/move.ts +31 -0
- package/src/tools/builtin/read.ts +69 -0
- package/src/tools/builtin/write.ts +42 -0
- package/src/tools/executor.ts +51 -0
- package/src/tools/external/database.ts +285 -0
- package/src/tools/external/enter-plan-mode.ts +34 -0
- package/src/tools/external/generate-image.ts +110 -0
- package/src/tools/external/hitl-tools.ts +118 -0
- package/src/tools/external/preview.ts +28 -0
- package/src/tools/external/proxy-fetch.ts +51 -0
- package/src/tools/external/task.ts +38 -0
- package/src/tools/external/wait.ts +20 -0
- package/src/tools/external/web-fetch.ts +57 -0
- package/src/tools/external/web-search.ts +61 -0
- package/src/tools/registry.ts +36 -0
- package/src/tools/zod-to-json-schema.ts +86 -0
- package/src/types.ts +151 -0
- package/test-hitl.sh +90 -0
- package/tsconfig.json +15 -0
package/hello.txt
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strayl/agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/agent.js --external:fsevents",
|
|
11
|
+
"dev": "tsx watch src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"openai": "^5.8.0",
|
|
15
|
+
"zod": "^3.25.67"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"esbuild": "^0.25.5",
|
|
19
|
+
"tsx": "^4.19.4",
|
|
20
|
+
"typescript": "^5.8.3",
|
|
21
|
+
"@types/node": "^22.15.31"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/run.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Strayl Agent Runner
|
|
4
|
+
# Usage: ./run.sh "your prompt here"
|
|
5
|
+
# Options:
|
|
6
|
+
# ./run.sh "prompt" - raw JSON output
|
|
7
|
+
# ./run.sh "prompt" --pretty - human-readable output
|
|
8
|
+
# ./run.sh "prompt" --model deep - use specific model (auto|light|pro|deep)
|
|
9
|
+
# ./run.sh "prompt" --work-dir /path - set working directory
|
|
10
|
+
|
|
11
|
+
# Direct mode: SDK calls providers directly with API keys (for local dev only)
|
|
12
|
+
# In production, STRAYL_LLM_DIRECT is NOT set — all calls go through api.strayl.dev
|
|
13
|
+
export STRAYL_LLM_DIRECT="1"
|
|
14
|
+
export GOOGLE_GENERATIVE_AI_API_KEY="AIzaSyBLeK4sA6RjoAk1f1011nvtaS0OxFO6nas"
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
PROMPT=""
|
|
18
|
+
MODEL="auto"
|
|
19
|
+
PRETTY=false
|
|
20
|
+
EXTRA_ARGS=()
|
|
21
|
+
|
|
22
|
+
while [[ $# -gt 0 ]]; do
|
|
23
|
+
case $1 in
|
|
24
|
+
--pretty) PRETTY=true; shift ;;
|
|
25
|
+
--model) MODEL="$2"; shift 2 ;;
|
|
26
|
+
--work-dir|--blocked-tools|--max-iterations|--extra-prompt-file|--skills-dir)
|
|
27
|
+
EXTRA_ARGS+=("$1" "$2"); shift 2 ;;
|
|
28
|
+
*)
|
|
29
|
+
if [[ -z "$PROMPT" ]]; then
|
|
30
|
+
PROMPT="$1"
|
|
31
|
+
fi
|
|
32
|
+
shift ;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
if [[ -z "$PROMPT" ]]; then
|
|
37
|
+
echo "Usage: ./run.sh \"your prompt\" [--pretty] [--model auto|light|pro|deep]"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if $PRETTY; then
|
|
42
|
+
node "$SCRIPT_DIR/dist/agent.js" --model "$MODEL" --prompt "$PROMPT" "${EXTRA_ARGS[@]}" 2>&1 | while IFS= read -r line; do
|
|
43
|
+
type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null)
|
|
44
|
+
case "$type" in
|
|
45
|
+
session-start)
|
|
46
|
+
model=$(echo "$line" | jq -r '.model')
|
|
47
|
+
echo -e "\033[36m▶ Session started (model: $model)\033[0m"
|
|
48
|
+
;;
|
|
49
|
+
text-delta)
|
|
50
|
+
echo -n "$(echo "$line" | jq -r '.text')"
|
|
51
|
+
;;
|
|
52
|
+
reasoning-delta)
|
|
53
|
+
echo -ne "\033[2m$(echo "$line" | jq -r '.text')\033[0m"
|
|
54
|
+
;;
|
|
55
|
+
tool-call-start)
|
|
56
|
+
name=$(echo "$line" | jq -r '.name')
|
|
57
|
+
args=$(echo "$line" | jq -c '.args')
|
|
58
|
+
echo -e "\n\033[33m🔧 $name\033[0m $args"
|
|
59
|
+
;;
|
|
60
|
+
tool-result)
|
|
61
|
+
echo -e "\033[32m✅ done\033[0m"
|
|
62
|
+
;;
|
|
63
|
+
exec-log)
|
|
64
|
+
echo -ne "\033[2m$(echo "$line" | jq -r '.data')\033[0m"
|
|
65
|
+
;;
|
|
66
|
+
usage-update)
|
|
67
|
+
left=$(echo "$line" | jq -r '.context_left_percent')
|
|
68
|
+
cost=$(echo "$line" | jq -r '.cost // 0')
|
|
69
|
+
;;
|
|
70
|
+
session-end)
|
|
71
|
+
reason=$(echo "$line" | jq -r '.exit_reason')
|
|
72
|
+
input=$(echo "$line" | jq -r '.usage.input_tokens')
|
|
73
|
+
output=$(echo "$line" | jq -r '.usage.output_tokens')
|
|
74
|
+
echo -e "\n\033[36m■ Done ($reason) | tokens: ${input}in/${output}out\033[0m"
|
|
75
|
+
;;
|
|
76
|
+
error)
|
|
77
|
+
msg=$(echo "$line" | jq -r '.message')
|
|
78
|
+
echo -e "\033[31m✖ $msg\033[0m"
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
done
|
|
82
|
+
else
|
|
83
|
+
node "$SCRIPT_DIR/dist/agent.js" --model "$MODEL" --prompt "$PROMPT" "${EXTRA_ARGS[@]}" 2>&1
|
|
84
|
+
fi
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import type { AgentConfig, ToolCall, ToolContext, Middleware, Message, AgentMode, ActivityType } from "./types.js";
|
|
2
|
+
import { Emitter } from "./emitter.js";
|
|
3
|
+
import { LLMClient } from "./llm/client.js";
|
|
4
|
+
import { getModelLimits, resolveModel } from "./llm/models.js";
|
|
5
|
+
import { ContextManager } from "./context/manager.js";
|
|
6
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
7
|
+
import { executeTool } from "./tools/executor.js";
|
|
8
|
+
import { HITLManager } from "./hitl/manager.js";
|
|
9
|
+
import { SubAgentManager } from "./subagents/manager.js";
|
|
10
|
+
import { TodoManager } from "./todos/manager.js";
|
|
11
|
+
import { buildSystemPrompt } from "./prompts/system.js";
|
|
12
|
+
import { loadSkills } from "./skills/loader.js";
|
|
13
|
+
import { StdinListener } from "./stdin-listener.js";
|
|
14
|
+
import { CheckpointManager } from "./checkpoints/manager.js";
|
|
15
|
+
|
|
16
|
+
// Builtin tools
|
|
17
|
+
import { execTool, getLogsTool } from "./tools/builtin/exec.js";
|
|
18
|
+
import { readFileTool } from "./tools/builtin/read.js";
|
|
19
|
+
import { writeFileTool } from "./tools/builtin/write.js";
|
|
20
|
+
import { editFileTool } from "./tools/builtin/edit.js";
|
|
21
|
+
import { lsTool } from "./tools/builtin/ls.js";
|
|
22
|
+
import { grepTool } from "./tools/builtin/grep.js";
|
|
23
|
+
import { globTool } from "./tools/builtin/glob.js";
|
|
24
|
+
import { deleteFileTool } from "./tools/builtin/delete.js";
|
|
25
|
+
import { moveFileTool } from "./tools/builtin/move.js";
|
|
26
|
+
|
|
27
|
+
// External tools
|
|
28
|
+
import { webSearchTool } from "./tools/external/web-search.js";
|
|
29
|
+
import { webFetchTool } from "./tools/external/web-fetch.js";
|
|
30
|
+
import { previewTool } from "./tools/external/preview.js";
|
|
31
|
+
import { waitTool } from "./tools/external/wait.js";
|
|
32
|
+
import { generateImageTool } from "./tools/external/generate-image.js";
|
|
33
|
+
import { createTaskTool } from "./tools/external/task.js";
|
|
34
|
+
import { enterPlanModeTool } from "./tools/external/enter-plan-mode.js";
|
|
35
|
+
import { askUserTool, writePlanTool, requestEnvVarTool } from "./tools/external/hitl-tools.js";
|
|
36
|
+
import { createDatabaseTool, listDatabasesTool, runDatabaseQueryTool, prepareDatabaseMigrationTool, completeDatabaseMigrationTool } from "./tools/external/database.js";
|
|
37
|
+
|
|
38
|
+
// Middleware
|
|
39
|
+
import { forbiddenPackagesMiddleware } from "./middleware/forbidden-packages.js";
|
|
40
|
+
import { toolCompressionMiddleware } from "./middleware/tool-compression.js";
|
|
41
|
+
import { createToolFilterMiddleware } from "./middleware/tool-filter.js";
|
|
42
|
+
import { createPromptCachingMiddleware } from "./middleware/prompt-caching.js";
|
|
43
|
+
import { credentialScrubbingMiddleware } from "./middleware/credential-scrubbing.js";
|
|
44
|
+
import { createPlanModeMiddleware, resetContextForImplementation } from "./middleware/plan-mode.js";
|
|
45
|
+
|
|
46
|
+
export async function runAgent(config: AgentConfig): Promise<void> {
|
|
47
|
+
const emitter = new Emitter();
|
|
48
|
+
const stdin = new StdinListener();
|
|
49
|
+
stdin.start();
|
|
50
|
+
const modelName = resolveModel(config.model);
|
|
51
|
+
const limits = getModelLimits(modelName);
|
|
52
|
+
|
|
53
|
+
// Mode state — mutable, changes on enterPlanMode tool or confirm-plan stdin command
|
|
54
|
+
let currentMode: AgentMode = config.mode ?? "normal";
|
|
55
|
+
|
|
56
|
+
const client = new LLMClient({
|
|
57
|
+
modelTier: config.model,
|
|
58
|
+
env: config.env,
|
|
59
|
+
sessionId: config.sessionId,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const context = new ContextManager({
|
|
63
|
+
maxInputTokens: limits.maxInputTokens,
|
|
64
|
+
summarizationThreshold: config.summarizationThreshold,
|
|
65
|
+
preSummarizationRatio: config.preSummarizationRatio,
|
|
66
|
+
previousSummary: config.previousSummary,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const registry = new ToolRegistry();
|
|
70
|
+
const hitl = new HITLManager(config.hitlDir);
|
|
71
|
+
await hitl.init();
|
|
72
|
+
const subAgentManager = new SubAgentManager(emitter);
|
|
73
|
+
const todoManager = new TodoManager(emitter);
|
|
74
|
+
const checkpoints = new CheckpointManager(config.workDir, config.sessionId);
|
|
75
|
+
await checkpoints.init();
|
|
76
|
+
|
|
77
|
+
// Register all tools
|
|
78
|
+
const builtinTools = [execTool, getLogsTool, readFileTool, writeFileTool, editFileTool, lsTool, grepTool, globTool, deleteFileTool, moveFileTool];
|
|
79
|
+
const databaseTools = [createDatabaseTool, listDatabasesTool, runDatabaseQueryTool, prepareDatabaseMigrationTool, completeDatabaseMigrationTool];
|
|
80
|
+
const externalTools = [webSearchTool, webFetchTool, previewTool, waitTool, generateImageTool, enterPlanModeTool, ...databaseTools];
|
|
81
|
+
const hitlTools = [askUserTool, writePlanTool, requestEnvVarTool];
|
|
82
|
+
|
|
83
|
+
for (const tool of [...builtinTools, ...externalTools, ...hitlTools]) {
|
|
84
|
+
registry.register(tool);
|
|
85
|
+
}
|
|
86
|
+
registry.register(createTaskTool(subAgentManager));
|
|
87
|
+
for (const tool of todoManager.createTools()) {
|
|
88
|
+
registry.register(tool);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build middleware
|
|
92
|
+
const middleware: Middleware[] = [
|
|
93
|
+
toolCompressionMiddleware,
|
|
94
|
+
forbiddenPackagesMiddleware,
|
|
95
|
+
credentialScrubbingMiddleware,
|
|
96
|
+
createToolFilterMiddleware(config.blockedTools ?? []),
|
|
97
|
+
createPlanModeMiddleware(() => currentMode),
|
|
98
|
+
createPromptCachingMiddleware(modelName),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
emitter.emit({ type: "session-start", model: modelName, session_id: config.sessionId });
|
|
102
|
+
|
|
103
|
+
// Load skills and build system prompt
|
|
104
|
+
const skills = await loadSkills(config.skillsDir ?? "./skills");
|
|
105
|
+
const systemPrompt = buildSystemPrompt({
|
|
106
|
+
workDir: config.workDir,
|
|
107
|
+
skills,
|
|
108
|
+
systemPromptExtra: config.systemPromptExtra,
|
|
109
|
+
mode: currentMode,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let iteration = 0;
|
|
113
|
+
const maxIterations = config.maxIterations ?? 200;
|
|
114
|
+
|
|
115
|
+
// Restore from checkpoint or start fresh
|
|
116
|
+
if (config.restoreCheckpoint) {
|
|
117
|
+
const cp = config.restoreCheckpoint;
|
|
118
|
+
context.restoreMessages(cp.messages);
|
|
119
|
+
todoManager.restore(cp.todos);
|
|
120
|
+
iteration = cp.iteration;
|
|
121
|
+
emitter.emit({ type: "checkpoint-restored", id: cp.id, iteration: cp.iteration });
|
|
122
|
+
|
|
123
|
+
// Inject new user message as continuation
|
|
124
|
+
context.addUser(config.userMessage);
|
|
125
|
+
} else {
|
|
126
|
+
context.addSystem(systemPrompt);
|
|
127
|
+
context.addUser(config.userMessage, config.images);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
while (iteration < maxIterations) {
|
|
131
|
+
iteration++;
|
|
132
|
+
|
|
133
|
+
// Process stdin commands (inject, cancel, hitl-response, rollback)
|
|
134
|
+
for (const cmd of stdin.drain()) {
|
|
135
|
+
switch (cmd.type) {
|
|
136
|
+
case "inject":
|
|
137
|
+
context.addUser(cmd.text, cmd.images);
|
|
138
|
+
emitter.emit({ type: "inject-received", text: cmd.text });
|
|
139
|
+
break;
|
|
140
|
+
case "hitl-response":
|
|
141
|
+
// Forward to file-based HITL (stdin is an alternative transport)
|
|
142
|
+
await hitl.writeResponse(cmd.id, { decision: cmd.decision, data: cmd.data });
|
|
143
|
+
break;
|
|
144
|
+
case "confirm-plan": {
|
|
145
|
+
if (currentMode !== "plan") break;
|
|
146
|
+
const prevMode = currentMode;
|
|
147
|
+
currentMode = "implement";
|
|
148
|
+
|
|
149
|
+
// Context reset: extract plan, clear planning messages, inject plan as system context
|
|
150
|
+
const { newMessages, planContent } = resetContextForImplementation(context.messages());
|
|
151
|
+
context.restoreMessages(newMessages);
|
|
152
|
+
|
|
153
|
+
// Save confirmed plan to disk
|
|
154
|
+
if (planContent) {
|
|
155
|
+
const planDir = `${config.workDir}/.strayl/plans`;
|
|
156
|
+
const fs = await import("node:fs/promises");
|
|
157
|
+
const path = await import("node:path");
|
|
158
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
159
|
+
const planFile = path.join(planDir, `${config.sessionId}-${Date.now()}.md`);
|
|
160
|
+
await fs.writeFile(planFile, planContent);
|
|
161
|
+
emitter.emit({ type: "plan-confirmed", plan: planContent });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
emitter.emit({ type: "mode-changed", from: prevMode, to: currentMode });
|
|
165
|
+
|
|
166
|
+
// Inject implementation mode prompt into context
|
|
167
|
+
const { IMPLEMENTATION_MODE_PROMPT } = await import("./prompts/system.js");
|
|
168
|
+
context.addUser(`[System] The plan has been confirmed. Switch to implementation mode.\n\n${IMPLEMENTATION_MODE_PROMPT}`);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case "rollback": {
|
|
172
|
+
const cp = cmd.checkpoint_id
|
|
173
|
+
? checkpoints.get(cmd.checkpoint_id)
|
|
174
|
+
: cmd.iteration != null
|
|
175
|
+
? checkpoints.getByIteration(cmd.iteration)
|
|
176
|
+
: checkpoints.latest();
|
|
177
|
+
if (cp) {
|
|
178
|
+
context.restoreMessages(cp.messages);
|
|
179
|
+
todoManager.restore(cp.todos);
|
|
180
|
+
iteration = cp.iteration;
|
|
181
|
+
emitter.emit({ type: "checkpoint-restored", id: cp.id, iteration: cp.iteration });
|
|
182
|
+
} else {
|
|
183
|
+
emitter.emit({ type: "error", message: "Checkpoint not found", recoverable: true });
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check cancellation (stdin or file-based)
|
|
191
|
+
if (stdin.isCancelled() || await hitl.isCancelled()) {
|
|
192
|
+
stdin.stop();
|
|
193
|
+
emitter.emit({ type: "session-end", usage: context.totalUsage(), exit_reason: "cancelled" });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Pre-summarization (non-blocking)
|
|
198
|
+
context.maybeTriggerPreSummarization(client, emitter);
|
|
199
|
+
|
|
200
|
+
// Apply pending summary if ready
|
|
201
|
+
await context.applyPendingSummary(emitter);
|
|
202
|
+
|
|
203
|
+
// Hard summarization (blocking)
|
|
204
|
+
if (context.shouldSummarize()) {
|
|
205
|
+
emitter.emit({
|
|
206
|
+
type: "summarizing",
|
|
207
|
+
token_count: context.estimateTokens(),
|
|
208
|
+
threshold: config.summarizationThreshold ?? 140_000,
|
|
209
|
+
});
|
|
210
|
+
await context.summarize(client, emitter);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Hard trim if still over limit
|
|
214
|
+
if (context.estimateTokens() > context.maxInputTokens) {
|
|
215
|
+
context.applyTrim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Prepare messages + tools
|
|
219
|
+
let messages = context.messages();
|
|
220
|
+
let tools = registry.toOpenAITools(new Set(config.blockedTools));
|
|
221
|
+
|
|
222
|
+
// Apply middleware
|
|
223
|
+
for (const mw of middleware) {
|
|
224
|
+
if (mw.beforeModel) messages = mw.beforeModel(messages);
|
|
225
|
+
if (mw.filterTools) tools = mw.filterTools(tools);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Stream LLM response
|
|
229
|
+
let assistantText = "";
|
|
230
|
+
const completedToolCalls: ToolCall[] = [];
|
|
231
|
+
const partialArgs = new Map<number, { id: string; name: string; args: string }>();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
for await (const chunk of client.stream(messages, tools)) {
|
|
235
|
+
switch (chunk.type) {
|
|
236
|
+
case "text":
|
|
237
|
+
assistantText += chunk.text;
|
|
238
|
+
emitter.emit({ type: "text-delta", text: chunk.text });
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case "reasoning":
|
|
242
|
+
emitter.emit({ type: "reasoning-delta", text: chunk.text });
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case "tool_call_delta": {
|
|
246
|
+
const partial = partialArgs.get(chunk.index) ?? { id: "", name: "", args: "" };
|
|
247
|
+
if (chunk.id) partial.id = chunk.id;
|
|
248
|
+
if (chunk.name) partial.name = chunk.name;
|
|
249
|
+
partial.args += chunk.arguments;
|
|
250
|
+
partialArgs.set(chunk.index, partial);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case "tool_call_complete":
|
|
255
|
+
completedToolCalls.push({
|
|
256
|
+
id: chunk.id,
|
|
257
|
+
type: "function",
|
|
258
|
+
function: { name: chunk.name, arguments: chunk.arguments },
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case "usage": {
|
|
263
|
+
context.recordUsage(chunk);
|
|
264
|
+
const used = context.estimateTokens();
|
|
265
|
+
const max = context.maxInputTokens;
|
|
266
|
+
const leftPercent = Math.max(0, Math.round((1 - used / max) * 100));
|
|
267
|
+
emitter.emit({
|
|
268
|
+
type: "usage-update",
|
|
269
|
+
input_tokens: chunk.input_tokens,
|
|
270
|
+
output_tokens: chunk.output_tokens,
|
|
271
|
+
cost: chunk.cost,
|
|
272
|
+
peak_input_tokens: context.peakInputTokens(),
|
|
273
|
+
context_left_percent: leftPercent,
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
281
|
+
emitter.emit({ type: "error", message: `LLM error: ${msg}`, recoverable: true });
|
|
282
|
+
|
|
283
|
+
// Add error as assistant message so loop can continue
|
|
284
|
+
context.addAssistant(`[Error communicating with model: ${msg}]`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Add assistant message to context
|
|
289
|
+
context.addAssistant(assistantText, completedToolCalls.length > 0 ? completedToolCalls : undefined);
|
|
290
|
+
|
|
291
|
+
// Emit context status after each LLM call
|
|
292
|
+
{
|
|
293
|
+
const used = context.estimateTokens();
|
|
294
|
+
const max = context.maxInputTokens;
|
|
295
|
+
const leftPercent = Math.max(0, Math.round((1 - used / max) * 100));
|
|
296
|
+
const usage = context.totalUsage();
|
|
297
|
+
emitter.emit({
|
|
298
|
+
type: "usage-update",
|
|
299
|
+
input_tokens: usage.input_tokens,
|
|
300
|
+
output_tokens: usage.output_tokens,
|
|
301
|
+
cost: usage.cost,
|
|
302
|
+
peak_input_tokens: context.peakInputTokens(),
|
|
303
|
+
context_left_percent: leftPercent,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// No tool calls = agent is done
|
|
308
|
+
if (completedToolCalls.length === 0) break;
|
|
309
|
+
|
|
310
|
+
// Execute tool calls
|
|
311
|
+
for (const tc of completedToolCalls) {
|
|
312
|
+
let parsedArgs: unknown;
|
|
313
|
+
try {
|
|
314
|
+
parsedArgs = JSON.parse(tc.function.arguments);
|
|
315
|
+
} catch {
|
|
316
|
+
parsedArgs = {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Emit semantic activity for UI indicators
|
|
320
|
+
const activity = toolToActivity(tc.function.name, parsedArgs);
|
|
321
|
+
if (activity) {
|
|
322
|
+
emitter.emit({ type: "activity", ...activity });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
emitter.emit({ type: "tool-call-start", id: tc.id, name: tc.function.name, args: parsedArgs });
|
|
326
|
+
|
|
327
|
+
// HITL interrupt check
|
|
328
|
+
const toolDef = registry.get(tc.function.name);
|
|
329
|
+
if (toolDef?.hitl) {
|
|
330
|
+
emitter.emit({ type: "hitl-request", id: tc.id, safe_id: hitl.safeId(tc.id), tool: tc.function.name, args: parsedArgs });
|
|
331
|
+
const response = await hitl.waitForResponse(tc.id);
|
|
332
|
+
|
|
333
|
+
if (response.decision === "reject") {
|
|
334
|
+
const rejectResult = JSON.stringify({ error: "User rejected this action." });
|
|
335
|
+
emitter.emit({ type: "tool-result", id: tc.id, name: tc.function.name, output: rejectResult });
|
|
336
|
+
context.addToolResult(tc.id, tc.function.name, rejectResult);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (response.decision === "edit" && response.data && typeof response.data === "object") {
|
|
341
|
+
parsedArgs = { ...(parsedArgs as Record<string, unknown>), ...(response.data as Record<string, unknown>) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
emitter.emit({ type: "hitl-response", id: tc.id, decision: response.decision, data: response.data });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Execute with middleware chain
|
|
348
|
+
const toolCtx: ToolContext = {
|
|
349
|
+
emitter,
|
|
350
|
+
workDir: config.workDir,
|
|
351
|
+
env: config.env,
|
|
352
|
+
sessionId: config.sessionId,
|
|
353
|
+
toolCallId: tc.id,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const modifiedTc: ToolCall = {
|
|
357
|
+
...tc,
|
|
358
|
+
function: { ...tc.function, arguments: JSON.stringify(parsedArgs) },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const result = await executeTool(registry, modifiedTc, toolCtx, middleware);
|
|
362
|
+
|
|
363
|
+
emitter.emit({ type: "tool-result", id: tc.id, name: tc.function.name, output: result });
|
|
364
|
+
context.addToolResult(tc.id, tc.function.name, result);
|
|
365
|
+
|
|
366
|
+
// Mode transition: enterPlanMode tool → switch to plan mode
|
|
367
|
+
if (tc.function.name === "enterPlanMode" && currentMode === "normal") {
|
|
368
|
+
const prevMode = currentMode;
|
|
369
|
+
currentMode = "plan";
|
|
370
|
+
emitter.emit({ type: "mode-changed", from: prevMode, to: currentMode });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Save checkpoint after each complete iteration (LLM response + all tool results)
|
|
375
|
+
await checkpoints.save(iteration, context.messages(), todoManager.read(), context.totalUsage(), emitter);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (iteration >= maxIterations) {
|
|
379
|
+
emitter.emit({
|
|
380
|
+
type: "error",
|
|
381
|
+
message: `Agent exceeded maximum iterations (${maxIterations})`,
|
|
382
|
+
recoverable: false,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
stdin.stop();
|
|
387
|
+
emitter.emit({
|
|
388
|
+
type: "session-end",
|
|
389
|
+
usage: context.totalUsage(),
|
|
390
|
+
exit_reason: iteration >= maxIterations ? "max_iterations" : "complete",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Map tool name → semantic activity for UI indicators */
|
|
395
|
+
function toolToActivity(
|
|
396
|
+
name: string,
|
|
397
|
+
args: unknown,
|
|
398
|
+
): { activity: ActivityType; message?: string } | null {
|
|
399
|
+
const a = args as Record<string, unknown> | undefined;
|
|
400
|
+
switch (name) {
|
|
401
|
+
case "write_todos": {
|
|
402
|
+
const todos = a?.todos as unknown[] | undefined;
|
|
403
|
+
if (!todos || todos.length === 0) return { activity: "clearing-todos" };
|
|
404
|
+
return { activity: "creating-todos" };
|
|
405
|
+
}
|
|
406
|
+
case "update_todo":
|
|
407
|
+
return { activity: "updating-todos" };
|
|
408
|
+
case "askUser":
|
|
409
|
+
return { activity: "asking-user", message: a?.question as string };
|
|
410
|
+
case "writePlan":
|
|
411
|
+
return { activity: "planning", message: a?.title as string };
|
|
412
|
+
case "enterPlanMode":
|
|
413
|
+
return { activity: "planning", message: a?.reason as string };
|
|
414
|
+
case "create_database":
|
|
415
|
+
return { activity: "creating-database", message: a?.name as string };
|
|
416
|
+
case "run_database_query":
|
|
417
|
+
return { activity: "running-query" };
|
|
418
|
+
case "prepare_database_migration":
|
|
419
|
+
case "complete_database_migration":
|
|
420
|
+
return { activity: "running-migration" };
|
|
421
|
+
case "web_search":
|
|
422
|
+
return { activity: "searching-web", message: a?.query as string };
|
|
423
|
+
case "web_fetch":
|
|
424
|
+
return { activity: "fetching-page", message: a?.url as string };
|
|
425
|
+
case "generate_image":
|
|
426
|
+
return { activity: "generating-image" };
|
|
427
|
+
case "read_file":
|
|
428
|
+
return { activity: "reading-file", message: a?.path as string };
|
|
429
|
+
case "write_file":
|
|
430
|
+
return { activity: "writing-file", message: a?.path as string };
|
|
431
|
+
case "edit_file":
|
|
432
|
+
return { activity: "editing-file", message: a?.path as string };
|
|
433
|
+
case "exec":
|
|
434
|
+
return { activity: "running-command", message: (a?.command as string)?.slice(0, 80) };
|
|
435
|
+
case "task":
|
|
436
|
+
return { activity: "delegating-task", message: a?.description as string };
|
|
437
|
+
default:
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Message, TodoItem, Usage, CheckpointData } from "../types.js";
|
|
4
|
+
import type { Emitter } from "../emitter.js";
|
|
5
|
+
|
|
6
|
+
export type Checkpoint = CheckpointData;
|
|
7
|
+
|
|
8
|
+
export class CheckpointManager {
|
|
9
|
+
private dir: string;
|
|
10
|
+
private checkpoints: Map<string, Checkpoint> = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(workDir: string, sessionId: string) {
|
|
13
|
+
this.dir = path.join(workDir, ".strayl", "checkpoints", sessionId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async init(): Promise<void> {
|
|
17
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// Load existing checkpoints from disk
|
|
20
|
+
try {
|
|
21
|
+
const files = await fs.readdir(this.dir);
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
if (!file.endsWith(".json")) continue;
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(path.join(this.dir, file), "utf-8");
|
|
26
|
+
const cp = JSON.parse(content) as Checkpoint;
|
|
27
|
+
this.checkpoints.set(cp.id, cp);
|
|
28
|
+
} catch {
|
|
29
|
+
// Corrupted checkpoint — skip
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// No checkpoints dir yet — fine
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async save(
|
|
38
|
+
iteration: number,
|
|
39
|
+
messages: Message[],
|
|
40
|
+
todos: TodoItem[],
|
|
41
|
+
usage: Usage,
|
|
42
|
+
emitter: Emitter,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
const id = `cp_${iteration}_${Date.now()}`;
|
|
45
|
+
const checkpoint: Checkpoint = {
|
|
46
|
+
id,
|
|
47
|
+
iteration,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
messages: structuredClone(messages),
|
|
50
|
+
todos: structuredClone(todos),
|
|
51
|
+
usage: { ...usage },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.checkpoints.set(id, checkpoint);
|
|
55
|
+
|
|
56
|
+
// Write atomically: tmp → rename
|
|
57
|
+
const tmpPath = path.join(this.dir, `${id}.tmp`);
|
|
58
|
+
const finalPath = path.join(this.dir, `${id}.json`);
|
|
59
|
+
await fs.writeFile(tmpPath, JSON.stringify(checkpoint));
|
|
60
|
+
await fs.rename(tmpPath, finalPath);
|
|
61
|
+
|
|
62
|
+
emitter.emit({ type: "checkpoint-saved", id, iteration, checkpoint });
|
|
63
|
+
|
|
64
|
+
// Keep only last 50 checkpoints on disk
|
|
65
|
+
await this.pruneOld(50);
|
|
66
|
+
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get(id: string): Checkpoint | undefined {
|
|
71
|
+
return this.checkpoints.get(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Get the most recent checkpoint at or before the given iteration */
|
|
75
|
+
getByIteration(iteration: number): Checkpoint | undefined {
|
|
76
|
+
let best: Checkpoint | undefined;
|
|
77
|
+
for (const cp of this.checkpoints.values()) {
|
|
78
|
+
if (cp.iteration <= iteration) {
|
|
79
|
+
if (!best || cp.iteration > best.iteration) {
|
|
80
|
+
best = cp;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Get the latest checkpoint */
|
|
88
|
+
latest(): Checkpoint | undefined {
|
|
89
|
+
let best: Checkpoint | undefined;
|
|
90
|
+
for (const cp of this.checkpoints.values()) {
|
|
91
|
+
if (!best || cp.timestamp > best.timestamp) {
|
|
92
|
+
best = cp;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
list(): Checkpoint[] {
|
|
99
|
+
return [...this.checkpoints.values()].sort((a, b) => a.iteration - b.iteration);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async pruneOld(keep: number): Promise<void> {
|
|
103
|
+
const sorted = this.list();
|
|
104
|
+
if (sorted.length <= keep) return;
|
|
105
|
+
|
|
106
|
+
const toRemove = sorted.slice(0, sorted.length - keep);
|
|
107
|
+
for (const cp of toRemove) {
|
|
108
|
+
this.checkpoints.delete(cp.id);
|
|
109
|
+
await fs.unlink(path.join(this.dir, `${cp.id}.json`)).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|