Skip to content

Commit 5aad9bb

Browse files
committed
refactor: ai agent behaviour and logic with lots of fixes
- organise the logics - show tool call on ui - fix ui issues - and a lot more
1 parent 772b70e commit 5aad9bb

File tree

6 files changed

+1583
-650
lines changed

6 files changed

+1583
-650
lines changed

AGENTS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- Source: `src/` (ES modules, SCSS). Key areas: `src/components/`, `src/pages/`, `src/lib/`, `src/plugins/` (Cordova plugins), `src/styles/`.
5+
- Web assets: `www/` (entry `index.html`, build output in `www/build/`, css/js subfolders).
6+
- Config: `webpack.config.js`, `.babelrc`, `biome.json`, `config.xml`.
7+
- Utilities: `utils/` (build, start, setup scripts; language and tooling helpers).
8+
9+
## Build, Test, and Development Commands
10+
- `pnpm run setup`: Install deps, add Android platform, create build dirs, add plugins.
11+
- `pnpm run start -- android d`: Build and run on device/emulator (default dev). For release-like: `pnpm run start -- android p`.
12+
- `pnpm run build`: Build app (defaults to dev). Production example: `pnpm run build -- paid prod`. F-Droid variant: `pnpm run build -- paid prod fdroid`.
13+
- `pnpm run clean -- android android`: Recreate Android platform.
14+
- `pnpm run plugin -- <id> [path]`: Reinstall a Cordova plugin.
15+
- Linting/format: `pnpm run lint`, `pnpm run format`, `pnpm run check` (Biome).
16+
17+
## Coding Style & Naming Conventions
18+
- Formatting: Biome enforced; indent with tabs (`biome.json`). Run `npm run format` before commits.
19+
- Language: ESM JavaScript, SCSS; prefer imports from `src` root (webpack `resolve.modules: ["node_modules", "src"]`).
20+
- Naming: camelCase for files/dirs (e.g., `fileSystem`, `quickTools`), PascalCase for classes/components, kebab-case for SCSS partials when applicable.
21+
22+
## Testing Guidelines
23+
- No formal test runner configured. Provide clear manual verification steps for Android (device/emulator), including affected screens, repro steps, and expected/actual behavior.
24+
- Attach logs when relevant (see issue templates mention of `Acode.log`). Screenshots or screen recordings are encouraged for UI changes.
25+
26+
## Commit & Pull Request Guidelines
27+
- Commits: Follow conventional style when possible (`feat:`, `fix:`, `chore(scope):`, etc.), reference issues (`(#123)`).
28+
- PRs: Include a concise summary, linked issues, screenshots for UI, and test/verification steps. Note any Cordova plugin or config changes.
29+
- Keep diffs focused; run `npm run lint` and ensure `www/build/` is generated by the pipeline, not committed.
30+
31+
## Security & Configuration Tips
32+
- Building for F-Droid: pass `fdroid` as the third arg to `build` to toggle plugin set.
33+
- Do not commit secrets/keystores. Android signing is handled outside the repo; keep `keystore.jks` private.
34+
35+
# When you need to call tools from the shell, use this rubric:
36+
37+
- Find Files: `fd`
38+
- Find Text: `rg` (ripgrep)
39+
- Select among matches: pipe to `fzf`
40+
- JSON: `jq`
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import {
2+
isAIMessageChunk,
3+
isToolMessage,
4+
isToolMessageChunk,
5+
} from "@langchain/core/messages";
6+
7+
function extractText(contentLike) {
8+
if (!contentLike) return "";
9+
if (typeof contentLike === "string") return contentLike;
10+
if (Array.isArray(contentLike)) {
11+
return contentLike
12+
.map((item) => {
13+
if (!item) return "";
14+
if (typeof item === "string") return item;
15+
if (typeof item === "object" && "text" in item) return item.text ?? "";
16+
if (typeof item === "object" && "content" in item)
17+
return extractText(item.content);
18+
return "";
19+
})
20+
.join("");
21+
}
22+
if (typeof contentLike === "object" && "text" in contentLike) {
23+
return contentLike.text ?? "";
24+
}
25+
return "";
26+
}
27+
28+
function ensureToolState(toolStates, toolCallId, defaults = {}) {
29+
if (!toolStates.has(toolCallId)) {
30+
toolStates.set(toolCallId, {
31+
id: toolCallId,
32+
name: defaults.name || "Tool",
33+
argsBuffer: "",
34+
argsObject: undefined,
35+
output: "",
36+
status: "running",
37+
isNew: true,
38+
stage: "start",
39+
});
40+
}
41+
const state = toolStates.get(toolCallId);
42+
Object.assign(state, defaults);
43+
return state;
44+
}
45+
46+
export function createSessionManager(agent) {
47+
return {
48+
async runTurn({
49+
conversationId,
50+
userMessage,
51+
signal,
52+
assistantMessageId,
53+
onStart,
54+
onToken,
55+
onToolEvent,
56+
}) {
57+
const toolStates = new Map();
58+
let accumulatedText = "";
59+
60+
const emitToolState = (state) => {
61+
if (!onToolEvent) return;
62+
state.parentMessageId = assistantMessageId;
63+
onToolEvent({
64+
id: state.id,
65+
name: state.name,
66+
argsText: state.argsBuffer,
67+
argsObject: state.argsObject,
68+
output: state.output,
69+
status: state.status,
70+
stage: state.stage,
71+
isNew: state.isNew,
72+
parentMessageId: assistantMessageId,
73+
});
74+
state.isNew = false;
75+
};
76+
77+
try {
78+
const stream = await agent.stream(
79+
{ messages: [userMessage] },
80+
{
81+
streamMode: "messages",
82+
signal,
83+
configurable: {
84+
thread_id: conversationId,
85+
},
86+
},
87+
);
88+
89+
onStart?.();
90+
91+
for await (const event of stream) {
92+
const payload = Array.isArray(event) ? event[0] : event;
93+
if (!payload) continue;
94+
95+
if (isToolMessageChunk(payload) || isToolMessage(payload)) {
96+
const toolCallId =
97+
payload.tool_call_id || payload.id || `tool_${toolStates.size}`;
98+
const state = ensureToolState(toolStates, toolCallId);
99+
const chunkText = extractText(payload.content ?? payload);
100+
if (chunkText) {
101+
state.output = `${state.output || ""}${chunkText}`;
102+
}
103+
if (payload.status) {
104+
state.status = payload.status === "error" ? "error" : "success";
105+
}
106+
state.stage = "output";
107+
emitToolState(state);
108+
continue;
109+
}
110+
111+
if (isAIMessageChunk(payload)) {
112+
const { tool_call_chunks: toolCallChunks, tool_calls: toolCalls } =
113+
payload;
114+
115+
if (Array.isArray(toolCallChunks) && toolCallChunks.length) {
116+
for (const chunk of toolCallChunks) {
117+
const toolCallId =
118+
chunk.id ||
119+
chunk.tool_call_id ||
120+
chunk.name ||
121+
`tool_${toolStates.size}`;
122+
const state = ensureToolState(toolStates, toolCallId, {
123+
name:
124+
chunk.name || toolStates.get(toolCallId)?.name || "Tool",
125+
});
126+
if (chunk.args) {
127+
state.argsBuffer = `${state.argsBuffer || ""}${chunk.args}`;
128+
}
129+
state.stage = "args-delta";
130+
emitToolState(state);
131+
}
132+
}
133+
134+
if (Array.isArray(toolCalls) && toolCalls.length) {
135+
for (const call of toolCalls) {
136+
const toolCallId =
137+
call.id || call.name || `tool_${toolStates.size}`;
138+
const state = ensureToolState(toolStates, toolCallId, {
139+
name: call.name || toolStates.get(toolCallId)?.name || "Tool",
140+
});
141+
state.argsObject = call.args;
142+
try {
143+
state.argsBuffer = JSON.stringify(call.args, null, 2);
144+
} catch (error) {
145+
state.argsBuffer = String(call.args);
146+
}
147+
state.stage = "args-final";
148+
emitToolState(state);
149+
}
150+
}
151+
}
152+
153+
const chunkText = extractText(payload?.content ?? payload);
154+
if (chunkText) {
155+
accumulatedText += chunkText;
156+
onToken?.({
157+
fullText: accumulatedText,
158+
delta: chunkText,
159+
});
160+
}
161+
}
162+
163+
const toolRuns = Array.from(toolStates.values()).map((state) => {
164+
if (!state.status || state.status === "running") {
165+
state.status = "success";
166+
}
167+
state.stage = "complete";
168+
emitToolState(state);
169+
return {
170+
id: state.id,
171+
name: state.name,
172+
status: state.status,
173+
argsText: state.argsBuffer,
174+
argsObject: state.argsObject,
175+
output: state.output,
176+
timestamp: Date.now(),
177+
parentMessageId: assistantMessageId,
178+
};
179+
});
180+
181+
return {
182+
content: accumulatedText,
183+
toolRuns,
184+
};
185+
} catch (error) {
186+
if (toolStates.size) {
187+
error.toolRuns = Array.from(toolStates.values()).map((state) => ({
188+
id: state.id,
189+
name: state.name,
190+
status: state.status,
191+
argsText: state.argsBuffer,
192+
argsObject: state.argsObject,
193+
output: state.output,
194+
timestamp: Date.now(),
195+
parentMessageId: assistantMessageId,
196+
}));
197+
}
198+
throw error;
199+
}
200+
},
201+
};
202+
}

0 commit comments

Comments
 (0)