@lazyingart/agintiflow 0.20.74 → 0.20.76

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.74",
3
+ "version": "0.20.76",
4
4
  "type": "module",
5
5
  "description": "Low-cost, project-aware Web and CLI agents with DeepSeek/Venice/OpenAI routing, visible tool calls, durable sessions, scouts, AAPS, SCS, and guarded local execution.",
6
6
  "license": "Apache-2.0",
@@ -78,6 +78,31 @@ try {
78
78
  };
79
79
  const repair = repairModelMessageHistory(staleDeepSeekState, { provider: "deepseek" });
80
80
  assert(repair.changed, "stale DeepSeek history was not repaired");
81
+
82
+ const interleavedToolState = {
83
+ messages: [
84
+ { role: "system", content: "system" },
85
+ { role: "user", content: "do guarded writes" },
86
+ {
87
+ role: "assistant",
88
+ content: "",
89
+ tool_calls: [
90
+ { id: "call-a", type: "function", function: { name: "write_file", arguments: "{\"path\":\".env\",\"content\":\"TOKEN=blocked\"}" } },
91
+ { id: "call-b", type: "function", function: { name: "write_file", arguments: "{\"path\":\"notes/ok.md\",\"content\":\"ok\"}" } },
92
+ ],
93
+ },
94
+ { role: "tool", tool_call_id: "call-a", content: "{\"ok\":false,\"blocked\":true}" },
95
+ { role: "user", content: "Loop guard: do not repeat the blocked call." },
96
+ { role: "tool", tool_call_id: "call-b", content: "{\"ok\":false,\"skipped\":true}" },
97
+ ],
98
+ };
99
+ const interleavedRepair = repairModelMessageHistory(interleavedToolState, { provider: "openai" });
100
+ assert(interleavedRepair.changed, "interleaved tool-call history repair did not report a change");
101
+ const roles = interleavedToolState.messages.map((message) => `${message.role}:${message.tool_call_id || ""}`);
102
+ assert(
103
+ roles.join("|") === "system:|user:|assistant:|tool:call-a|tool:call-b|user:",
104
+ `interleaved tool-call history was not repaired into provider-valid order: ${roles.join("|")}`
105
+ );
81
106
  assert(
82
107
  staleDeepSeekState.messages.every(
83
108
  (message) => message.role !== "assistant" || message.reasoning_content || message.reasoningContent
@@ -254,6 +279,28 @@ try {
254
279
  });
255
280
  assert(permissionAdvice.suggestedCommand.includes("coding-policy-smoke"), "permission advice did not include resume session id");
256
281
  assert(permissionAdvice.suggestedCommand.includes("--sandbox-mode docker-workspace"), "permission advice did not suggest docker-workspace recovery");
282
+ const destructiveAdvice = buildPermissionAdvice({
283
+ toolName: "run_command",
284
+ args: { command: "rm -rf reports && git reset --hard" },
285
+ guard: { category: "destructive", reason: "Destructive shell commands require Allow destructive actions." },
286
+ config: dockerWorkspacePolicy,
287
+ state: { sessionId: "coding-destructive-smoke" },
288
+ });
289
+ const destructiveAdviceText = [
290
+ destructiveAdvice.summary,
291
+ ...(destructiveAdvice.options || []),
292
+ destructiveAdvice.suggestedCommand,
293
+ destructiveAdvice.destructiveApprovalCommand,
294
+ ].join("\n");
295
+ assert(/dry-run|inspect-only/i.test(destructiveAdviceText), "destructive advice did not lead with dry-run or inspect-only alternatives");
296
+ assert(
297
+ !destructiveAdvice.suggestedCommand.includes("--allow-destructive"),
298
+ "default destructive advice suggested command should not enable destructive mode"
299
+ );
300
+ assert(
301
+ destructiveAdvice.destructiveApprovalCommand.includes("--allow-destructive"),
302
+ "destructive advice did not provide an explicit approval command"
303
+ );
257
304
  const failedNetworkAdvice = buildFailedCommandAdvice({
258
305
  args: { command: "git clone https://github.com/lazyingart/AgInTiFlow.git" },
259
306
  commandPolicy: clonePolicy,
@@ -596,6 +643,7 @@ try {
596
643
  workspace,
597
644
  checks: [
598
645
  "deepseek_history_repair",
646
+ "interleaved_tool_history_repair",
599
647
  "blocked_tool_batch_short_circuit",
600
648
  "deepseek_pro_patch_route",
601
649
  "runtime_time_context",
@@ -213,14 +213,23 @@ function normalizeUrlPath(relativePath) {
213
213
  function preserveAssistantMessage(message) {
214
214
  const preserved = {
215
215
  role: "assistant",
216
- content: message.content || "",
217
- tool_calls: message.tool_calls,
216
+ content: redactSensitiveText(message.content || ""),
217
+ tool_calls: Array.isArray(message.tool_calls)
218
+ ? message.tool_calls.map((call) => ({
219
+ ...call,
220
+ function: {
221
+ ...(call.function || {}),
222
+ arguments:
223
+ typeof call.function?.arguments === "string"
224
+ ? redactSensitiveText(call.function.arguments)
225
+ : call.function?.arguments,
226
+ },
227
+ }))
228
+ : message.tool_calls,
218
229
  };
219
230
 
220
231
  const reasoningContent = message.reasoning_content || message.reasoningContent;
221
- if (reasoningContent) {
222
- preserved.reasoning_content = reasoningContent;
223
- }
232
+ if (reasoningContent) preserved.reasoning_content = redactSensitiveText(reasoningContent);
224
233
 
225
234
  return preserved;
226
235
  }
@@ -233,6 +242,7 @@ export function repairModelMessageHistory(state, config = {}) {
233
242
  convertedAssistantMessages: 0,
234
243
  droppedToolMessages: 0,
235
244
  incompleteToolCallMessages: 0,
245
+ reorderedInterleavedMessages: 0,
236
246
  };
237
247
  }
238
248
 
@@ -241,6 +251,7 @@ export function repairModelMessageHistory(state, config = {}) {
241
251
  let convertedAssistantMessages = 0;
242
252
  let droppedToolMessages = 0;
243
253
  let incompleteToolCallMessages = 0;
254
+ let reorderedInterleavedMessages = 0;
244
255
 
245
256
  for (let index = 0; index < state.messages.length; index += 1) {
246
257
  const message = state.messages[index];
@@ -254,12 +265,21 @@ export function repairModelMessageHistory(state, config = {}) {
254
265
  const expectedIds = toolCalls.map((call) => String(call?.id || "")).filter(Boolean);
255
266
  const expected = new Set(expectedIds);
256
267
  const followingToolMessages = [];
268
+ const deferredInterleavedMessages = [];
257
269
  const duplicateOrUnexpectedToolMessages = [];
258
270
  const seen = new Set();
259
271
  let cursor = index + 1;
260
272
 
261
- while (cursor < state.messages.length && state.messages[cursor]?.role === "tool") {
262
- const toolMessage = state.messages[cursor];
273
+ while (cursor < state.messages.length) {
274
+ const nextMessage = state.messages[cursor];
275
+ if (nextMessage?.role !== "tool") {
276
+ const complete = expectedIds.length > 0 && expectedIds.every((id) => seen.has(id));
277
+ if (complete || nextMessage?.role === "assistant") break;
278
+ deferredInterleavedMessages.push(nextMessage);
279
+ cursor += 1;
280
+ continue;
281
+ }
282
+ const toolMessage = nextMessage;
263
283
  const toolCallId = String(toolMessage.tool_call_id || "");
264
284
  if (expected.has(toolCallId) && !seen.has(toolCallId)) {
265
285
  followingToolMessages.push(toolMessage);
@@ -275,12 +295,18 @@ export function repairModelMessageHistory(state, config = {}) {
275
295
  droppedAssistantMessages += 1;
276
296
  droppedToolMessages += followingToolMessages.length + duplicateOrUnexpectedToolMessages.length;
277
297
  if (!completeToolResults) incompleteToolCallMessages += 1;
298
+ if (deferredInterleavedMessages.length) {
299
+ repaired.push(...deferredInterleavedMessages);
300
+ reorderedInterleavedMessages += deferredInterleavedMessages.length;
301
+ }
278
302
  index = cursor - 1;
279
303
  continue;
280
304
  }
281
305
 
282
306
  repaired.push(preserveAssistantMessage(message));
283
307
  repaired.push(...followingToolMessages);
308
+ repaired.push(...deferredInterleavedMessages);
309
+ reorderedInterleavedMessages += deferredInterleavedMessages.length;
284
310
  droppedToolMessages += duplicateOrUnexpectedToolMessages.length;
285
311
  index = cursor - 1;
286
312
  continue;
@@ -320,6 +346,7 @@ export function repairModelMessageHistory(state, config = {}) {
320
346
  droppedAssistantMessages > 0 ||
321
347
  convertedAssistantMessages > 0 ||
322
348
  droppedToolMessages > 0 ||
349
+ reorderedInterleavedMessages > 0 ||
323
350
  repaired.length !== state.messages.length;
324
351
 
325
352
  if (changed) {
@@ -332,6 +359,7 @@ export function repairModelMessageHistory(state, config = {}) {
332
359
  convertedAssistantMessages,
333
360
  droppedToolMessages,
334
361
  incompleteToolCallMessages,
362
+ reorderedInterleavedMessages,
335
363
  };
336
364
  }
337
365
 
@@ -1372,7 +1400,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
1372
1400
  return result;
1373
1401
  }
1374
1402
  case "finish":
1375
- return { ok: true, done: true, result: String(args.result || ""), toolName: "finish" };
1403
+ return { ok: true, done: true, result: redactSensitiveText(String(args.result || "")), toolName: "finish" };
1376
1404
  default:
1377
1405
  throw new Error(`Unknown tool: ${toolCall.function.name}`);
1378
1406
  }
@@ -1481,7 +1509,7 @@ export async function runAgent(config) {
1481
1509
  taskProfile: config.taskProfile,
1482
1510
  goal: config.goal,
1483
1511
  });
1484
- state.plan = scsPlan.plan;
1512
+ state.plan = redactSensitiveText(scsPlan.plan);
1485
1513
  state.meta.scs = scsPlan.scs;
1486
1514
  state.messages.push({
1487
1515
  role: "user",
@@ -1496,7 +1524,7 @@ export async function runAgent(config) {
1496
1524
  await store.appendEvent("scs.committee.plan_drafted", {
1497
1525
  phase: scsPlan.scs.phase,
1498
1526
  phaseGoal: scsPlan.scs.phaseGoal,
1499
- plan: scsPlan.plan,
1527
+ plan: state.plan,
1500
1528
  acceptanceCriteria: scsPlan.scs.acceptanceCriteria,
1501
1529
  });
1502
1530
  await store.appendEvent(`scs.student.${scsPlan.scs.student.decision}`, scsPlan.scs.student);
@@ -1504,16 +1532,16 @@ export async function runAgent(config) {
1504
1532
  phase: scsPlan.scs.phase,
1505
1533
  phaseGoal: scsPlan.scs.phaseGoal,
1506
1534
  });
1507
- await store.savePlan(scsPlan.plan);
1508
- await store.appendEvent("plan.created", { plan: scsPlan.plan, scs: true });
1535
+ await store.savePlan(state.plan);
1536
+ await store.appendEvent("plan.created", { plan: state.plan, scs: true });
1509
1537
  await store.saveState(state);
1510
- observers.event("plan.created", { plan: scsPlan.plan, scs: true });
1538
+ observers.event("plan.created", { plan: state.plan, scs: true });
1511
1539
  observers.event("scs.student.approve_plan", scsPlan.scs.student);
1512
1540
  emitConsole(config, `SCS: student approved phase plan (${Math.round((scsPlan.scs.student.confidence || 0) * 100)}%).`, {
1513
1541
  kind: "meta",
1514
1542
  });
1515
1543
  } else {
1516
- const plan = await createPlan(client, config, state);
1544
+ const plan = redactSensitiveText(await createPlan(client, config, state));
1517
1545
  state.plan = plan;
1518
1546
  await store.savePlan(plan);
1519
1547
  await store.appendEvent("plan.created", { plan });
@@ -1718,7 +1746,7 @@ export async function runAgent(config) {
1718
1746
 
1719
1747
  await store.appendEvent("model.responded", {
1720
1748
  step,
1721
- content: assistantMessage.content || "",
1749
+ content: redactSensitiveText(assistantMessage.content || ""),
1722
1750
  toolCalls: (assistantMessage.tool_calls || []).map((call) => ({
1723
1751
  id: call.id,
1724
1752
  name: call.function.name,
@@ -1727,7 +1755,7 @@ export async function runAgent(config) {
1727
1755
  });
1728
1756
  observers.event("model.responded", {
1729
1757
  step,
1730
- content: assistantMessage.content || "",
1758
+ content: redactSensitiveText(assistantMessage.content || ""),
1731
1759
  });
1732
1760
 
1733
1761
  const toolCalls = assistantMessage.tool_calls || [];
@@ -1740,7 +1768,7 @@ export async function runAgent(config) {
1740
1768
  await store.saveState(state);
1741
1769
  continue;
1742
1770
  }
1743
- const fallback = assistantMessage.content?.trim() || "No tool call returned.";
1771
+ const fallback = redactSensitiveText(assistantMessage.content?.trim() || "No tool call returned.");
1744
1772
  if (config.scsActive) {
1745
1773
  const decision = await reviewScsFinish(client, config, state, fallback, {
1746
1774
  events: await store.loadEvents(),
@@ -1792,6 +1820,7 @@ export async function runAgent(config) {
1792
1820
  }
1793
1821
 
1794
1822
  let continueForQueuedInput = false;
1823
+ const postBatchToolResults = [];
1795
1824
  for (let toolIndex = 0; toolIndex < toolCalls.length; toolIndex += 1) {
1796
1825
  const toolCall = toolCalls[toolIndex];
1797
1826
  throwIfAborted(config);
@@ -1801,44 +1830,7 @@ export async function runAgent(config) {
1801
1830
  tool_call_id: toolCall.id,
1802
1831
  content: JSON.stringify(toolResult),
1803
1832
  });
1804
- await applyToolLoopGuard(state, toolResult, store, observers);
1805
-
1806
- if (config.scsActive && shouldReviewToolResult(toolResult, state)) {
1807
- const decision = await reviewScsToolResult(client, config, state, toolResult, {
1808
- events: await store.loadEvents(),
1809
- taskProfile: config.taskProfile,
1810
- goal: config.goal,
1811
- });
1812
- state.meta.scs = state.meta.scs || { enabled: true, mode: config.enableScs || "on", active: true };
1813
- state.meta.scs.monitorReviews = (state.meta.scs.monitorReviews || 0) + 1;
1814
- state.meta.scs.lastStudentDecision = decision;
1815
- await store.appendEvent(`scs.student.${decision.decision}`, {
1816
- ...decision,
1817
- toolName: toolResult.toolName,
1818
- });
1819
- observers.event(`scs.student.${decision.decision}`, {
1820
- decision: decision.decision,
1821
- reason: decision.reason,
1822
- toolName: toolResult.toolName,
1823
- });
1824
- if (decision.decision === "rethink_plan" || decision.decision === "reject_phase") {
1825
- state.messages.push({
1826
- role: "user",
1827
- content: [
1828
- "SCS student monitor requested a rethink based on tool evidence.",
1829
- `Decision: ${decision.decision}`,
1830
- `Reason: ${decision.reason || "No reason provided."}`,
1831
- decision.nextRequiredAction ? `Next required action: ${decision.nextRequiredAction}` : "",
1832
- "Supervisor: do not repeat the same failed call. Adjust within the approved phase or finish with a concrete blocker if the phase is invalidated.",
1833
- ]
1834
- .filter(Boolean)
1835
- .join("\n"),
1836
- });
1837
- emitConsole(config, `SCS: ${decision.decision} after ${toolResult.toolName}: ${decision.reason || "reviewed"}`, {
1838
- kind: "meta",
1839
- });
1840
- }
1841
- }
1833
+ postBatchToolResults.push(toolResult);
1842
1834
 
1843
1835
  if (toolResult.toolName === "run_command") {
1844
1836
  observers.log("command.output", {
@@ -1956,6 +1948,47 @@ export async function runAgent(config) {
1956
1948
  }
1957
1949
  }
1958
1950
 
1951
+ for (const toolResult of postBatchToolResults) {
1952
+ await applyToolLoopGuard(state, toolResult, store, observers);
1953
+
1954
+ if (config.scsActive && shouldReviewToolResult(toolResult, state)) {
1955
+ const decision = await reviewScsToolResult(client, config, state, toolResult, {
1956
+ events: await store.loadEvents(),
1957
+ taskProfile: config.taskProfile,
1958
+ goal: config.goal,
1959
+ });
1960
+ state.meta.scs = state.meta.scs || { enabled: true, mode: config.enableScs || "on", active: true };
1961
+ state.meta.scs.monitorReviews = (state.meta.scs.monitorReviews || 0) + 1;
1962
+ state.meta.scs.lastStudentDecision = decision;
1963
+ await store.appendEvent(`scs.student.${decision.decision}`, {
1964
+ ...decision,
1965
+ toolName: toolResult.toolName,
1966
+ });
1967
+ observers.event(`scs.student.${decision.decision}`, {
1968
+ decision: decision.decision,
1969
+ reason: decision.reason,
1970
+ toolName: toolResult.toolName,
1971
+ });
1972
+ if (decision.decision === "rethink_plan" || decision.decision === "reject_phase") {
1973
+ state.messages.push({
1974
+ role: "user",
1975
+ content: [
1976
+ "SCS student monitor requested a rethink based on tool evidence.",
1977
+ `Decision: ${decision.decision}`,
1978
+ `Reason: ${decision.reason || "No reason provided."}`,
1979
+ decision.nextRequiredAction ? `Next required action: ${decision.nextRequiredAction}` : "",
1980
+ "Supervisor: do not repeat the same failed call. Adjust within the approved phase or finish with a concrete blocker if the phase is invalidated.",
1981
+ ]
1982
+ .filter(Boolean)
1983
+ .join("\n"),
1984
+ });
1985
+ emitConsole(config, `SCS: ${decision.decision} after ${toolResult.toolName}: ${decision.reason || "reviewed"}`, {
1986
+ kind: "meta",
1987
+ });
1988
+ }
1989
+ }
1990
+ }
1991
+
1959
1992
  if (continueForQueuedInput) continue;
1960
1993
 
1961
1994
  if (config.scsActive && shouldReviewScsProgress(step, state)) {
@@ -69,6 +69,7 @@ export function formatBehaviorContractForPrompt({ mode = "runtime" } = {}) {
69
69
  "Make surgical edits: no drive-by refactors, unrelated formatting churn, or deletion of code you did not need to touch.",
70
70
  "Define or infer concrete success criteria for non-trivial work, then run focused checks or state why checks are unavailable.",
71
71
  "Respect the permission contract: if a tool is blocked or returns permissionAdvice, stop and present the exact suggestedCommand/approval path instead of retrying variants or inventing CLI flags.",
72
+ "When destructive cleanup is blocked, lead with inspect-only or dry-run evidence (`git status`, `git clean -nd`, targeted file lists). Do not call delete/reset/clean commands safe unless they are clearly labeled as requiring explicit user approval.",
72
73
  "Protect secrets aggressively: never repeat token/key/password/secret values from prompts, files, tool output, plans, final answers, reports, diffs, or artifacts. Redact the value as [REDACTED] and use dedicated key storage such as `aginti keys set` when credentials are needed.",
73
74
  "Keep artifacts durable and discoverable with descriptive non-conflicting names; never overwrite unless the user clearly asked.",
74
75
  "When reporting shell, language, runtime, build, or test results, name the actual environment used (host vs Docker, relevant interpreter/tool path/version when it matters). Do not claim compatibility across untested runtimes, hosts, containers, or language versions; state the caveat or run an explicit check.",
@@ -6,6 +6,7 @@ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
6
6
  import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
7
7
  import { platformInfo, platformLabel } from "./platform.js";
8
8
  import { formatBehaviorContractForPrompt } from "./behavior-contract.js";
9
+ import { redactSensitiveText } from "./redaction.js";
9
10
 
10
11
  export function createClient(config) {
11
12
  if (config.provider === "mock") {
@@ -558,7 +559,7 @@ export async function createPlan(client, config, state) {
558
559
  requestOptions(config)
559
560
  );
560
561
 
561
- return response.choices[0]?.message?.content?.trim() || "1. Inspect the page.\n2. Use the smallest safe action.\n3. Finish with a concise answer.";
562
+ return redactSensitiveText(response.choices[0]?.message?.content?.trim() || "1. Inspect the page.\n2. Use the smallest safe action.\n3. Finish with a concise answer.");
562
563
  }
563
564
 
564
565
  export async function requestNextStep(client, config, messages) {
@@ -135,7 +135,40 @@ function adviceForCategory(category = "", { toolName = "", args = {}, config = {
135
135
  };
136
136
  }
137
137
 
138
- if (category === "destructive" || category === "permission-change" || category === "general-shell") {
138
+ if (category === "destructive") {
139
+ return {
140
+ ...base,
141
+ summary:
142
+ "This command is destructive and was blocked. Do not retry variants. First offer inspect-only or dry-run cleanup evidence; destructive cleanup requires explicit user approval.",
143
+ options: [
144
+ "Inspect only: run non-destructive checks such as `git status --short`, `git clean -nd`, `find <path> -maxdepth ... -print`, or targeted file listings.",
145
+ "Safer cleanup plan: write a report listing exact files that would be removed, then ask the user before deletion.",
146
+ "Explicit destructive approval: only rerun with --allow-destructive after the user accepts the data-loss risk.",
147
+ ],
148
+ suggestedCommand: resumeCommand({
149
+ config,
150
+ state,
151
+ sandboxMode: "docker-workspace",
152
+ prompt: "Continue with inspect-only or dry-run cleanup evidence. Do not delete, reset, clean, or overwrite anything unless the user explicitly approves destructive actions.",
153
+ }),
154
+ destructiveApprovalCommand: resumeCommand({
155
+ config,
156
+ state,
157
+ sandboxMode: "docker-workspace",
158
+ destructive: true,
159
+ prompt: "Continue after the user explicitly approved destructive project-local cleanup. Inspect first, delete only the named targets, and verify git status afterwards.",
160
+ }),
161
+ trustedHostCommand: resumeCommand({
162
+ config,
163
+ state,
164
+ sandboxMode: "host",
165
+ destructive: true,
166
+ prompt: "Continue after the user explicitly approved trusted host destructive execution. Inspect first, avoid unrelated files, and keep git status understandable.",
167
+ }),
168
+ };
169
+ }
170
+
171
+ if (category === "permission-change" || category === "general-shell") {
139
172
  return {
140
173
  ...base,
141
174
  summary: