@agentprojectcontext/apx 1.32.2 → 1.33.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/package.json +1 -1
- package/src/core/agent/prompts/action-discipline.md +12 -5
- package/src/core/agent/prompts/channels/telegram.md +9 -5
- package/src/core/stores/code-sessions.js +4 -1
- package/src/host/daemon/api/artifacts.js +25 -0
- package/src/host/daemon/api/code.js +14 -1
- package/src/host/daemon/api/exec.js +17 -2
- package/src/host/daemon/plugins/telegram/index.js +2 -14
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +1 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +602 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/App.tsx +3 -1
- package/src/interfaces/web/src/components/UiSelect.tsx +12 -2
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +253 -111
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +10 -8
- package/src/interfaces/web/src/components/code/CodeComposer.tsx +20 -17
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +43 -18
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +212 -0
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +121 -0
- package/src/interfaces/web/src/components/code/CodeSessionList.tsx +30 -26
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +23 -19
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +140 -0
- package/src/interfaces/web/src/components/common/TabLayout.tsx +3 -3
- package/src/interfaces/web/src/components/ui/chat-input.tsx +17 -6
- package/src/interfaces/web/src/hooks/useChat.ts +1 -0
- package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +25 -1
- package/src/interfaces/web/src/i18n/es.ts +1 -1
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/artifacts.ts +10 -0
- package/src/interfaces/web/src/lib/api/code.ts +4 -2
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +423 -79
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +7 -10
- package/src/core/util/text-similarity.js +0 -52
- package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +0 -1
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js +0 -570
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +0 -1
package/package.json
CHANGED
|
@@ -5,11 +5,18 @@
|
|
|
5
5
|
- If you cannot execute the action (missing permission, unclear params, tool not available), explain WHY — do not promise and disappear.
|
|
6
6
|
- If the user asks you to do multiple things, do them all in the same turn using sequential tool calls if needed.
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
8
|
+
## Two-segment turns with tools — intro short, answer substantive (mandatory)
|
|
9
|
+
A turn that calls one or more tools produces TWO text segments shown to the user:
|
|
10
|
+
|
|
11
|
+
1. **Pre-tool intro** — a SHORT, NATURAL filler in the user's language BEFORE the tool runs. 2 to 8 words. NEVER contains the answer / data / acknowledgment. Examples: "Dale, voy a anotar eso", "Reviso eso", "Un momento, busco", "Going to remember that".
|
|
12
|
+
2. **Post-tool answer** — the SUBSTANTIVE result AFTER the tool returns. Carries the data, the confirmation, or the next question. Examples: "Listo, anoté que sos Tech Lead en Bytetravel.", "Encontré 3 routines activas: …".
|
|
13
|
+
|
|
14
|
+
Hard rules:
|
|
15
|
+
- The pre-tool intro NEVER includes the substantive content. Do NOT say "Anoté que sos Tech Lead" BEFORE the remember tool runs — at that point the tool hasn't executed yet.
|
|
16
|
+
- The post-tool answer NEVER restates what the intro already said. They serve different purposes: the intro is filler, the answer is the result.
|
|
17
|
+
- Greet AT MOST ONCE per turn. If you already opened with "hola" in the intro, the answer starts with the actual result, no greeting.
|
|
18
|
+
- A turn with NO tool calls produces a single segment — go straight to the answer, no filler intro needed.
|
|
19
|
+
- A simple chit-chat reply (no tool) is one segment: the reply itself.
|
|
13
20
|
|
|
14
21
|
## Chit-chat & greetings (only path out of a forced tool turn)
|
|
15
22
|
- If the user is just greeting, chatting, or thanking you with NO actionable request ("hola", "hi", "buenas", "gracias", "👍", "ok"), you must STILL satisfy the tool-choice contract: call `finish` with a brief friendly reply in the user's language. Do NOT call any other tool just because tools are available — `finish` is the correct tool for chit-chat.
|
|
@@ -6,9 +6,13 @@ Formatting:
|
|
|
6
6
|
- Keep replies brief (~6 sentences unless user asks for more)
|
|
7
7
|
- Previous turns are conversational context only; re-call tools for facts
|
|
8
8
|
|
|
9
|
-
What the user sees here:
|
|
9
|
+
What the user sees here: only your text segments. They do NOT see your tool calls, args, or intermediate results — those never reach Telegram.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
11
|
+
Two-segment turn (intro + answer):
|
|
12
|
+
- When you call a tool, write a SHORT natural intro BEFORE the tool runs (2–8 words in the user's language: "Dale, voy a anotar eso", "Reviso eso", "Un momento, busco esos datos"). That lands as a Telegram message of its own so the user sees you're working.
|
|
13
|
+
- AFTER the tool returns, write the substantive answer with the actual result or confirmation. That is the second Telegram message.
|
|
14
|
+
- The intro NEVER contains the substantive content — at that point the tool hasn't run yet, so you don't know the result. Wrong: "¡Anotado! Sos Tech Lead en Bytetravel" BEFORE remember runs. Right: "Dale, voy a anotar eso" before, then "Listo, anoté que sos Tech Lead." after.
|
|
15
|
+
- The answer NEVER restates the intro. They're complementary: filler + result, not the same content twice.
|
|
16
|
+
- Greet at most ONCE per turn. If the intro opened with "Hola", the answer starts with the result, no second greeting.
|
|
17
|
+
|
|
18
|
+
Turns without tools (small talk, "hola", "gracias"): a single message — the reply itself, no intro filler.
|
|
@@ -49,6 +49,7 @@ function toRow(s) {
|
|
|
49
49
|
title: s.title,
|
|
50
50
|
mode: s.mode,
|
|
51
51
|
model: s.model || null,
|
|
52
|
+
agentSlug: s.agentSlug || null,
|
|
52
53
|
createdAt: s.createdAt,
|
|
53
54
|
updatedAt: s.updatedAt,
|
|
54
55
|
messageCount: Array.isArray(s.messages) ? s.messages.length : 0,
|
|
@@ -78,7 +79,7 @@ export function getCodeSession(storagePath, id) {
|
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* Create a new session.
|
|
81
|
-
* fields: { projectId, title?, model?, mode?, git? }
|
|
82
|
+
* fields: { projectId, title?, model?, mode?, git?, agentSlug? }
|
|
82
83
|
*/
|
|
83
84
|
export function createCodeSession(storagePath, fields = {}) {
|
|
84
85
|
const id = shortId();
|
|
@@ -91,6 +92,7 @@ export function createCodeSession(storagePath, fields = {}) {
|
|
|
91
92
|
updatedAt: ts,
|
|
92
93
|
model: fields.model || null,
|
|
93
94
|
mode: fields.mode === "plan" ? "plan" : "build",
|
|
95
|
+
agentSlug: fields.agentSlug || null,
|
|
94
96
|
git: fields.git && typeof fields.git === "object" ? fields.git : null,
|
|
95
97
|
messages: [],
|
|
96
98
|
};
|
|
@@ -108,6 +110,7 @@ export function updateCodeSession(storagePath, id, patch = {}) {
|
|
|
108
110
|
if (patch.title != null) session.title = String(patch.title).trim() || session.title;
|
|
109
111
|
if (patch.model !== undefined) session.model = patch.model || null;
|
|
110
112
|
if (patch.mode === "plan" || patch.mode === "build") session.mode = patch.mode;
|
|
113
|
+
if (patch.agentSlug !== undefined) session.agentSlug = patch.agentSlug || null;
|
|
111
114
|
if (patch.git !== undefined) session.git = patch.git;
|
|
112
115
|
session.updatedAt = nowIso();
|
|
113
116
|
writeJson(sessionFile(storagePath, id), session);
|
|
@@ -119,6 +119,31 @@ export function register(app, { project }) {
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
app.patch("/projects/:pid/artifacts/:name", (req, res) => {
|
|
123
|
+
const p = project(req, res);
|
|
124
|
+
if (!p) return;
|
|
125
|
+
const name = decodeURIComponent(req.params.name);
|
|
126
|
+
const { content, newName } = req.body || {};
|
|
127
|
+
try {
|
|
128
|
+
const absPath = artifactPath(p.storagePath, name);
|
|
129
|
+
if (!fs.existsSync(absPath)) {
|
|
130
|
+
return res.status(404).json({ error: `artifact "${name}" not found` });
|
|
131
|
+
}
|
|
132
|
+
if (typeof content === "string") {
|
|
133
|
+
fs.writeFileSync(absPath, content, "utf8");
|
|
134
|
+
}
|
|
135
|
+
let finalName = name;
|
|
136
|
+
if (newName && newName !== name) {
|
|
137
|
+
const newAbsPath = artifactPath(p.storagePath, newName);
|
|
138
|
+
fs.renameSync(absPath, newAbsPath);
|
|
139
|
+
finalName = newName;
|
|
140
|
+
}
|
|
141
|
+
res.json({ ok: true, name: finalName });
|
|
142
|
+
} catch (e) {
|
|
143
|
+
res.status(400).json({ error: e.message });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
122
147
|
app.delete("/projects/:pid/artifacts/:name", (req, res) => {
|
|
123
148
|
const p = project(req, res);
|
|
124
149
|
if (!p) return;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "#core/stores/code-sessions.js";
|
|
27
27
|
import { captureBaseline, diffAgainstBaseline, initGitRepo } from "#core/git-baseline.js";
|
|
28
28
|
import { loggerFor } from "#core/logging.js";
|
|
29
|
+
import { readAgents } from "#core/apc/parser.js";
|
|
29
30
|
|
|
30
31
|
const log = loggerFor("code");
|
|
31
32
|
|
|
@@ -212,7 +213,7 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
212
213
|
app.post("/projects/:pid/code/sessions", (req, res) => {
|
|
213
214
|
const p = findProject(req, res);
|
|
214
215
|
if (!p) return;
|
|
215
|
-
const { title, model, mode } = req.body || {};
|
|
216
|
+
const { title, model, mode, agentSlug } = req.body || {};
|
|
216
217
|
let git = captureBaseline(p.path);
|
|
217
218
|
// No baseline because the project isn't a git repo yet. For real projects
|
|
218
219
|
// (not the default apx home, id 0) init one so the "changes" diff works —
|
|
@@ -230,6 +231,7 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
230
231
|
title,
|
|
231
232
|
model,
|
|
232
233
|
mode,
|
|
234
|
+
agentSlug: agentSlug || null,
|
|
233
235
|
git,
|
|
234
236
|
});
|
|
235
237
|
res.status(201).json(session);
|
|
@@ -291,6 +293,15 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
291
293
|
const mode = session.mode === "plan" ? "plan" : "build";
|
|
292
294
|
const previousMessages = historyFrom(session);
|
|
293
295
|
|
|
296
|
+
// If a project agent is selected, inject its system prompt as a suffix so
|
|
297
|
+
// the super-agent's tool loop runs with the agent's personality/context.
|
|
298
|
+
let agentSystemSuffix = "";
|
|
299
|
+
if (session.agentSlug) {
|
|
300
|
+
const agents = readAgents(p.path);
|
|
301
|
+
const agent = agents.find((a) => a.slug === session.agentSlug);
|
|
302
|
+
if (agent?.body) agentSystemSuffix = `\n\n## Agente seleccionado: ${session.agentSlug}\n${agent.body}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
294
305
|
// Persist the user turn immediately so a crash mid-stream still records it.
|
|
295
306
|
appendTurn(p.storagePath, session.id, {
|
|
296
307
|
role: "user",
|
|
@@ -324,8 +335,10 @@ export function register(app, { projects, project, config, registries, plugins }
|
|
|
324
335
|
projectPath: p.path,
|
|
325
336
|
mode,
|
|
326
337
|
modeGuidance: modeGuidanceFor(mode),
|
|
338
|
+
agentSlug: session.agentSlug || null,
|
|
327
339
|
},
|
|
328
340
|
previousMessages,
|
|
341
|
+
systemSuffix: agentSystemSuffix,
|
|
329
342
|
overrideModel: session.model || undefined,
|
|
330
343
|
allowedTools: mode === "plan" ? PLAN_TOOLS : "*",
|
|
331
344
|
// Coding tasks are multi-step: give the loop a high safety ceiling so it
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { callEngine } from "#core/engines/index.js";
|
|
8
8
|
import { readAgents } from "#core/apc/parser.js";
|
|
9
9
|
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
10
|
+
import { resolveActiveModel } from "#core/agent/model-router.js";
|
|
10
11
|
import {
|
|
11
12
|
startConversation,
|
|
12
13
|
appendTurn,
|
|
@@ -14,6 +15,20 @@ import {
|
|
|
14
15
|
setStatus,
|
|
15
16
|
} from "../conversations.js";
|
|
16
17
|
|
|
18
|
+
// Pick a model for a direct agent chat: explicit override → agent's own model →
|
|
19
|
+
// super-agent default (resolved via the same router the super-agent uses, so
|
|
20
|
+
// it walks the fallback chain when the primary is empty/unhealthy).
|
|
21
|
+
async function pickAgentModel({ modelOverride, agent, config }) {
|
|
22
|
+
if (modelOverride) return modelOverride;
|
|
23
|
+
if (agent.fields?.Model) return agent.fields.Model;
|
|
24
|
+
try {
|
|
25
|
+
const routing = await resolveActiveModel(config);
|
|
26
|
+
return routing?.modelId || null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
export function register(app, { projects, project, config }) {
|
|
18
33
|
app.post("/projects/:pid/agents/:slug/exec", async (req, res) => {
|
|
19
34
|
const p = project(req, res);
|
|
@@ -28,7 +43,7 @@ export function register(app, { projects, project, config }) {
|
|
|
28
43
|
const agents = readAgents(p.path);
|
|
29
44
|
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
30
45
|
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
31
|
-
const modelId = modelOverride
|
|
46
|
+
const modelId = await pickAgentModel({ modelOverride, agent, config });
|
|
32
47
|
if (!modelId)
|
|
33
48
|
return res
|
|
34
49
|
.status(400)
|
|
@@ -106,7 +121,7 @@ export function register(app, { projects, project, config }) {
|
|
|
106
121
|
const agents = readAgents(p.path);
|
|
107
122
|
const agent = agents.find((a) => a.slug === req.params.slug);
|
|
108
123
|
if (!agent) return res.status(404).json({ error: "agent not found" });
|
|
109
|
-
const modelId = modelOverride
|
|
124
|
+
const modelId = await pickAgentModel({ modelOverride, agent, config });
|
|
110
125
|
if (!modelId)
|
|
111
126
|
return res
|
|
112
127
|
.status(400)
|
|
@@ -44,7 +44,6 @@ import { buildRelationshipBlock } from "#core/agent/index.js";
|
|
|
44
44
|
import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
|
|
45
45
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
46
46
|
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
47
|
-
import { isLikelyDuplicate } from "#core/util/text-similarity.js";
|
|
48
47
|
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
49
48
|
import * as askFlow from "./ask.js";
|
|
50
49
|
|
|
@@ -645,12 +644,6 @@ class ChannelPoller {
|
|
|
645
644
|
if (ev.type === "assistant_text" && ev.text) {
|
|
646
645
|
const piece = stripThinking(ev.text).trim();
|
|
647
646
|
if (!piece) return;
|
|
648
|
-
// Skip post-tool segments that just restate the pre-tool intro —
|
|
649
|
-
// weaker models (gemini-flash et al.) frequently paraphrase the
|
|
650
|
-
// same content twice within a single turn.
|
|
651
|
-
if (lastStreamedText && isLikelyDuplicate(piece, lastStreamedText)) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
647
|
await this._send({ chat_id, text: piece });
|
|
655
648
|
lastStreamedText = piece;
|
|
656
649
|
streamedCount += 1;
|
|
@@ -798,13 +791,8 @@ class ChannelPoller {
|
|
|
798
791
|
// turn isn't silently empty.
|
|
799
792
|
const finalClean = replyText ? stripThinking(replyText).trim() : "";
|
|
800
793
|
let toSend = "";
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (finalClean && !isLikelyDuplicate(finalClean, lastStreamedText) && finalClean !== lastStreamedText) {
|
|
804
|
-
toSend = finalClean;
|
|
805
|
-
} else if (!finalClean && streamedCount === 0) {
|
|
806
|
-
toSend = "Listo.";
|
|
807
|
-
}
|
|
794
|
+
if (finalClean && finalClean !== lastStreamedText) toSend = finalClean;
|
|
795
|
+
else if (!finalClean && streamedCount === 0) toSend = "Listo.";
|
|
808
796
|
|
|
809
797
|
stopTyping();
|
|
810
798
|
if (!toSend) return; // everything was already streamed — nothing left to send
|