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.
@@ -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";
@@ -30,7 +30,7 @@ import {
30
30
  createWikiTools,
31
31
  createWebSearchTools,
32
32
  } from "../tools";
33
- import { clearTodoState } from "../tools/todo.ts";
33
+ import { clearTodoState } from "../state/todos.ts";
34
34
 
35
35
  const SECTION_BREAK = "\n\n---\n\n";
36
36
 
@@ -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) || config.tools.length === 0) {
36
- throw new Error(`Agent '${config.id}' must declare at least one tool.`);
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 assertKnownTools(
46
+ function filterKnownTools(
47
47
  definitions: readonly AgentDefinition[],
48
48
  knownTools: readonly string[],
49
- ): void {
49
+ ): AgentDefinition[] {
50
50
  const known = new Set(knownTools);
51
- for (const definition of definitions) {
52
- for (const toolName of definition.tools) {
53
- if (!known.has(toolName)) {
54
- throw new Error(
55
- `Agent '${definition.id}' references unknown tool '${toolName}'.`,
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
- assertKnownTools(definitions, options.knownTools);
103
+ return filterKnownTools(definitions, options.knownTools);
106
104
  }
107
105
  return definitions;
108
106
  }
@@ -33,7 +33,7 @@ const DEFAULT_PROMPTS = {
33
33
  behaviour: true,
34
34
  communication: true,
35
35
  execution: true,
36
- engineering: false,
36
+ engineering: true,
37
37
  guardrails: true,
38
38
  } as const;
39
39
 
@@ -1,6 +1,6 @@
1
- ## Engineering Judgment
1
+ ## Coding / Software Engineering
2
2
 
3
- Use senior engineering judgment, but let the repository guide the solution. Prefer local patterns over invented architecture.
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 repository has a test pattern for it.
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
- ### Repository Hygiene
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
+ }