hoomanjs 1.17.3 → 1.18.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 +1 -1
- package/package.json +3 -1
- package/src/acp/approvals.ts +1 -1
- package/src/acp/utils/tool-kind.ts +1 -1
- package/src/chat/app.tsx +57 -36
- package/src/chat/approvals.ts +1 -1
- package/src/chat/components/TodoPanel.tsx +1 -1
- package/src/chat/components/ToolEvent.tsx +7 -0
- package/src/chat/components/ToolEventFileResult.tsx +116 -0
- package/src/chat/components/TranscriptViewport.tsx +170 -0
- package/src/chat/components/file-tool-diff/file-tool-result.ts +155 -0
- package/src/chat/components/prompt-input/usePromptInputController.ts +5 -0
- package/src/chat/mouse.ts +17 -0
- package/src/chat/types.ts +3 -0
- package/src/core/agent/index.ts +1 -1
- package/src/core/agents/registry.ts +12 -14
- package/src/core/config.ts +1 -1
- package/src/core/prompts/harness/engineering.md +4 -4
- package/src/core/prompts/system.ts +13 -0
- package/src/core/state/file-tool-display.ts +70 -0
- package/src/core/state/thought-process.ts +49 -0
- package/src/core/state/todos.ts +84 -0
- package/src/core/tools/filesystem.ts +369 -26
- package/src/core/tools/thinking.ts +5 -40
- package/src/core/tools/todo.ts +6 -79
- package/src/daemon/approvals.ts +1 -1
- package/src/exec/approvals.ts +1 -1
- /package/src/core/{approvals/allowed-tools.ts → state/tool-approvals.ts} +0 -0
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { getPromptView, type PromptView } from "./render.ts";
|
|
35
35
|
import { getCwd } from "../../../core/utils/cwd-context.ts";
|
|
36
36
|
import { saveClipboardImageAsAttachment } from "./clipboard-image.ts";
|
|
37
|
+
import { isMouseInput } from "../../mouse.ts";
|
|
37
38
|
|
|
38
39
|
export type PromptSubmission = {
|
|
39
40
|
text: string;
|
|
@@ -355,6 +356,10 @@ export function usePromptInputController({
|
|
|
355
356
|
|
|
356
357
|
useInput(
|
|
357
358
|
(input, key) => {
|
|
359
|
+
if (isMouseInput(input)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
358
363
|
const state = getState();
|
|
359
364
|
|
|
360
365
|
if (key.return) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseMouseEvents } from "ink-use-mouse";
|
|
2
|
+
|
|
3
|
+
const STRIPPED_SGR_MOUSE_RE = /\[?<\d+;\d+;\d+[mM]/;
|
|
4
|
+
|
|
5
|
+
export const MOUSE_REPORTING_ENABLE =
|
|
6
|
+
"\x1b[?1003l\x1b[?1002l\x1b[?1000h\x1b[?1006h";
|
|
7
|
+
|
|
8
|
+
export const MOUSE_REPORTING_DISABLE =
|
|
9
|
+
"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l";
|
|
10
|
+
|
|
11
|
+
export function isMouseInput(input: string): boolean {
|
|
12
|
+
return (
|
|
13
|
+
parseMouseEvents(input).length > 0 || STRIPPED_SGR_MOUSE_RE.test(input)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { parseMouseEvents };
|
package/src/chat/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FileToolDisplay } from "../core/state/file-tool-display.ts";
|
|
2
|
+
|
|
1
3
|
export type ChatRole = "user" | "assistant" | "tool" | "system";
|
|
2
4
|
|
|
3
5
|
export interface ChatLine {
|
|
@@ -9,6 +11,7 @@ export interface ChatLine {
|
|
|
9
11
|
toolName?: string;
|
|
10
12
|
phase?: "running" | "done";
|
|
11
13
|
resultContent?: string;
|
|
14
|
+
fileToolDisplay?: FileToolDisplay;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export type ApprovalDecision = "allow" | "reject" | "always";
|
package/src/core/agent/index.ts
CHANGED
|
@@ -32,8 +32,8 @@ function validateConfigs(configs: readonly AgentConfig[]): void {
|
|
|
32
32
|
if (!config.description.trim()) {
|
|
33
33
|
throw new Error(`Agent '${config.id}' description cannot be empty.`);
|
|
34
34
|
}
|
|
35
|
-
if (!Array.isArray(config.tools)
|
|
36
|
-
throw new Error(`Agent '${config.id}' must
|
|
35
|
+
if (!Array.isArray(config.tools)) {
|
|
36
|
+
throw new Error(`Agent '${config.id}' tools must be an array.`);
|
|
37
37
|
}
|
|
38
38
|
for (const toolName of config.tools) {
|
|
39
39
|
if (!toolName.trim()) {
|
|
@@ -43,20 +43,18 @@ function validateConfigs(configs: readonly AgentConfig[]): void {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function
|
|
46
|
+
function filterKnownTools(
|
|
47
47
|
definitions: readonly AgentDefinition[],
|
|
48
48
|
knownTools: readonly string[],
|
|
49
|
-
):
|
|
49
|
+
): AgentDefinition[] {
|
|
50
50
|
const known = new Set(knownTools);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
}
|
|
51
|
+
return definitions.map((definition) => {
|
|
52
|
+
const tools = definition.tools.filter((toolName) => known.has(toolName));
|
|
53
|
+
return {
|
|
54
|
+
...definition,
|
|
55
|
+
tools,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
function context(config: Config): Record<string, unknown> {
|
|
@@ -102,7 +100,7 @@ export function loadBuiltInAgentDefinitions(
|
|
|
102
100
|
};
|
|
103
101
|
});
|
|
104
102
|
if (options?.knownTools) {
|
|
105
|
-
|
|
103
|
+
return filterKnownTools(definitions, options.knownTools);
|
|
106
104
|
}
|
|
107
105
|
return definitions;
|
|
108
106
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
## Engineering
|
|
1
|
+
## Coding / Software Engineering
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Handle coding tasks like a senior software engineer, but let the project guide the solution. Prefer local patterns over invented architecture.
|
|
4
4
|
|
|
5
5
|
### Code Changes
|
|
6
6
|
|
|
@@ -27,12 +27,12 @@ Use senior engineering judgment, but let the repository guide the solution. Pref
|
|
|
27
27
|
- Prefer structured parsers and APIs for structured data instead of ad hoc string manipulation.
|
|
28
28
|
- Treat generated files, lockfiles, migrations, and configuration as shared contracts. Update them only when the task requires it.
|
|
29
29
|
- Do not hide failures with broad catches, silent fallbacks, skipped hooks, or weakened checks.
|
|
30
|
-
- When touching shared behavior, add or update focused tests when the
|
|
30
|
+
- When touching shared behavior, add or update focused tests when the project has a test pattern for it.
|
|
31
31
|
- Avoid time estimates. Focus on what needs to happen and what is done.
|
|
32
32
|
- If an approach fails, diagnose the failure before switching tactics. Do not blindly retry the same step.
|
|
33
33
|
- Escalate with a focused user question only after investigation when safe progress is blocked.
|
|
34
34
|
|
|
35
|
-
###
|
|
35
|
+
### Project Hygiene
|
|
36
36
|
|
|
37
37
|
- Work with the current working tree. Do not revert user changes unless explicitly asked.
|
|
38
38
|
- If unexpected changes affect the task, inspect them and adapt. Ask only when they make safe progress impossible.
|
|
@@ -34,6 +34,7 @@ const HARNESS_PROMPT_FILES = [
|
|
|
34
34
|
export type SystemMode = "default" | "daemon" | "acp";
|
|
35
35
|
|
|
36
36
|
const SECTION_BREAK = "\n\n---\n\n";
|
|
37
|
+
const EXTRA_CWD_INSTRUCTIONS = "AGENTS.md";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Loads `prompts/static/*.md` from the package, then `instructions.md` from disk,
|
|
@@ -122,12 +123,21 @@ export class System {
|
|
|
122
123
|
return parts.join("\n\n");
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
private readCwdAgentsInstructions(): string {
|
|
127
|
+
const path = join(process.cwd(), EXTRA_CWD_INSTRUCTIONS);
|
|
128
|
+
if (!existsSync(path)) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
return readFileSync(path, "utf8").trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
private readRawText(): string {
|
|
126
135
|
const instructions = existsSync(this.path)
|
|
127
136
|
? readFileSync(this.path, "utf8").trim()
|
|
128
137
|
: "";
|
|
129
138
|
const bundled = this.readBundledStaticPrompts();
|
|
130
139
|
const harness = this.readBundledHarnessPrompts();
|
|
140
|
+
const extra = this.readCwdAgentsInstructions();
|
|
131
141
|
|
|
132
142
|
const blocks: string[] = [];
|
|
133
143
|
if (bundled.length > 0) {
|
|
@@ -139,6 +149,9 @@ export class System {
|
|
|
139
149
|
if (instructions.length > 0) {
|
|
140
150
|
blocks.push(instructions);
|
|
141
151
|
}
|
|
152
|
+
if (extra.length > 0) {
|
|
153
|
+
blocks.push(extra);
|
|
154
|
+
}
|
|
142
155
|
if (blocks.length === 0) {
|
|
143
156
|
return "";
|
|
144
157
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { JSONValue } from "@strands-agents/sdk";
|
|
2
|
+
|
|
3
|
+
const FILE_TOOL_DISPLAY_STATE_KEY = "fileToolDisplay";
|
|
4
|
+
|
|
5
|
+
export type StructuredPatchHunk = {
|
|
6
|
+
oldStart: number;
|
|
7
|
+
oldLines: number;
|
|
8
|
+
newStart: number;
|
|
9
|
+
newLines: number;
|
|
10
|
+
lines: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type FileToolDisplay = {
|
|
14
|
+
previews?: string[];
|
|
15
|
+
structuredPatch?: StructuredPatchHunk[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type DisplayStateStore = {
|
|
19
|
+
get(key: string): JSONValue | undefined;
|
|
20
|
+
set(key: string, value: unknown): void;
|
|
21
|
+
delete(key: string): void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function readDisplayState(
|
|
25
|
+
appState: DisplayStateStore,
|
|
26
|
+
): Record<string, FileToolDisplay> {
|
|
27
|
+
const value = appState.get(FILE_TOOL_DISPLAY_STATE_KEY);
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
29
|
+
? (value as Record<string, FileToolDisplay>)
|
|
30
|
+
: {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setFileToolDisplay(
|
|
34
|
+
appState: DisplayStateStore,
|
|
35
|
+
toolUseId: string | undefined,
|
|
36
|
+
display: FileToolDisplay,
|
|
37
|
+
): void {
|
|
38
|
+
if (!toolUseId) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
appState.set(FILE_TOOL_DISPLAY_STATE_KEY, {
|
|
43
|
+
...readDisplayState(appState),
|
|
44
|
+
[toolUseId]: display,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function takeFileToolDisplay(
|
|
49
|
+
appState: DisplayStateStore,
|
|
50
|
+
toolUseId: string | null,
|
|
51
|
+
): FileToolDisplay | undefined {
|
|
52
|
+
if (!toolUseId) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const state = readDisplayState(appState);
|
|
57
|
+
const display = state[toolUseId];
|
|
58
|
+
if (!display) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { [toolUseId]: _removed, ...nextState } = state;
|
|
63
|
+
if (Object.keys(nextState).length === 0) {
|
|
64
|
+
appState.delete(FILE_TOOL_DISPLAY_STATE_KEY);
|
|
65
|
+
} else {
|
|
66
|
+
appState.set(FILE_TOOL_DISPLAY_STATE_KEY, nextState);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return display;
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ToolContext } from "@strands-agents/sdk";
|
|
2
|
+
|
|
3
|
+
const THINKING_STATE_KEY = "thinking.sequential";
|
|
4
|
+
|
|
5
|
+
export type ThoughtEntry = {
|
|
6
|
+
thought: string;
|
|
7
|
+
thoughtNumber: number;
|
|
8
|
+
totalThoughts: number;
|
|
9
|
+
nextThoughtNeeded: boolean;
|
|
10
|
+
isRevision: boolean;
|
|
11
|
+
revisesThought: number | null;
|
|
12
|
+
branchFromThought: number | null;
|
|
13
|
+
branchId: string | null;
|
|
14
|
+
needsMoreThoughts: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ThinkingState = {
|
|
18
|
+
history: ThoughtEntry[];
|
|
19
|
+
branches: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isThinkingState(value: unknown): value is ThinkingState {
|
|
23
|
+
return (
|
|
24
|
+
value !== null &&
|
|
25
|
+
typeof value === "object" &&
|
|
26
|
+
!Array.isArray(value) &&
|
|
27
|
+
"history" in value &&
|
|
28
|
+
"branches" in value &&
|
|
29
|
+
Array.isArray(value.history) &&
|
|
30
|
+
Array.isArray(value.branches)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readThinkingState(context: ToolContext): ThinkingState {
|
|
35
|
+
const current = context.agent.appState.get(THINKING_STATE_KEY);
|
|
36
|
+
|
|
37
|
+
if (isThinkingState(current)) {
|
|
38
|
+
return current;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { history: [], branches: [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeThinkingState(
|
|
45
|
+
context: ToolContext,
|
|
46
|
+
state: ThinkingState,
|
|
47
|
+
): void {
|
|
48
|
+
context.agent.appState.set(THINKING_STATE_KEY, state);
|
|
49
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const TODO_ITEMS_STATE_KEY = "todo.items";
|
|
4
|
+
export const TODO_VISIBLE_STATE_KEY = "todo.visible";
|
|
5
|
+
|
|
6
|
+
export const TodoStatusSchema = z.enum(["pending", "in_progress", "completed"]);
|
|
7
|
+
|
|
8
|
+
export const TodoItemSchema = z.object({
|
|
9
|
+
content: z.string().trim().min(1),
|
|
10
|
+
status: TodoStatusSchema,
|
|
11
|
+
activeForm: z.string().trim().min(1),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type TodoStatus = z.infer<typeof TodoStatusSchema>;
|
|
15
|
+
export type TodoItem = z.infer<typeof TodoItemSchema>;
|
|
16
|
+
|
|
17
|
+
type AppStateLike = {
|
|
18
|
+
get<T = unknown>(key: string): T;
|
|
19
|
+
set(key: string, value: unknown): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AgentLike = {
|
|
23
|
+
appState: AppStateLike;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TodoViewState = {
|
|
27
|
+
visible: boolean;
|
|
28
|
+
todos: TodoItem[];
|
|
29
|
+
total: number;
|
|
30
|
+
pending: number;
|
|
31
|
+
ongoing: number;
|
|
32
|
+
completed: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function normalizeTodoItems(value: unknown): TodoItem[] {
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const normalized: TodoItem[] = [];
|
|
40
|
+
for (const item of value) {
|
|
41
|
+
const parsed = TodoItemSchema.safeParse(item);
|
|
42
|
+
if (parsed.success) {
|
|
43
|
+
normalized.push(parsed.data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function summarizeTodos(
|
|
50
|
+
todos: TodoItem[],
|
|
51
|
+
): Omit<TodoViewState, "visible" | "todos"> {
|
|
52
|
+
const pending = todos.filter((todo) => todo.status === "pending").length;
|
|
53
|
+
const ongoing = todos.filter((todo) => todo.status === "in_progress").length;
|
|
54
|
+
const completed = todos.filter((todo) => todo.status === "completed").length;
|
|
55
|
+
return {
|
|
56
|
+
total: todos.length,
|
|
57
|
+
pending,
|
|
58
|
+
ongoing,
|
|
59
|
+
completed,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTodoViewState(agent: AgentLike): TodoViewState {
|
|
64
|
+
const todos = normalizeTodoItems(agent.appState.get(TODO_ITEMS_STATE_KEY));
|
|
65
|
+
const rawVisible = agent.appState.get(TODO_VISIBLE_STATE_KEY);
|
|
66
|
+
const visible =
|
|
67
|
+
typeof rawVisible === "boolean" ? rawVisible : todos.length > 0;
|
|
68
|
+
const summary = summarizeTodos(todos);
|
|
69
|
+
return {
|
|
70
|
+
visible,
|
|
71
|
+
todos,
|
|
72
|
+
...summary,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function setTodoState(agent: AgentLike, todos: TodoItem[]): void {
|
|
77
|
+
agent.appState.set(TODO_ITEMS_STATE_KEY, todos);
|
|
78
|
+
agent.appState.set(TODO_VISIBLE_STATE_KEY, todos.length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearTodoState(agent: AgentLike): void {
|
|
82
|
+
agent.appState.set(TODO_ITEMS_STATE_KEY, []);
|
|
83
|
+
agent.appState.set(TODO_VISIBLE_STATE_KEY, false);
|
|
84
|
+
}
|