hoomanjs 1.17.4 → 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
  }
@@ -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
+ }