🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@iinm/plain-agent

Package Overview
Dependencies
Maintainers
1
Versions
120
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@iinm/plain-agent - npm Package Compare versions

Comparing version
1.13.1
to
1.14.0
+28
src/tokenUsage.mjs
/**
* @import { ProviderTokenUsage } from "./model"
*/
/**
* Extract the input (prompt/context) token count from a single turn's
* provider token usage object by summing the values of the specified keys.
*
* Returns `undefined` when no specified key yields a positive number.
*
* @param {ProviderTokenUsage} usage
* @param {string[]} inputTokensKeys - Keys whose numeric values are summed.
* @returns {number | undefined}
*/
export function extractInputTokenCount(usage, inputTokensKeys) {
let total = 0;
let found = false;
for (const key of inputTokensKeys) {
const value = usage[key];
if (typeof value === "number" && value > 0) {
total += value;
found = true;
}
}
return found ? total : undefined;
}
+150
-42
{
"autoCompact": { "softLimit": 120000 },
"autoApproval": {

@@ -157,3 +158,6 @@ "defaultAction": "ask",

"desc": "find should be denied",
"toolUse": { "toolName": "exec_command", "input": { "command": "find" } },
"toolUse": {
"toolName": "exec_command",
"input": { "command": "find" }
},
"expectedAction": "deny"

@@ -163,3 +167,6 @@ },

"desc": "grep should be denied",
"toolUse": { "toolName": "exec_command", "input": { "command": "grep" } },
"toolUse": {
"toolName": "exec_command",
"input": { "command": "grep" }
},
"expectedAction": "deny"

@@ -367,3 +374,8 @@ },

"command": "gh",
"args": ["api", "--method", "GET", "repos/owner/repo/pulls/123/comments"]
"args": [
"api",
"--method",
"GET",
"repos/owner/repo/pulls/123/comments"
]
}

@@ -377,3 +389,6 @@ },

"toolName": "exec_command",
"input": { "command": "gh", "args": ["api", "repos/owner/repo/pulls"] }
"input": {
"command": "gh",
"args": ["api", "repos/owner/repo/pulls"]
}
},

@@ -405,3 +420,6 @@ "expectedAction": "deny"

"toolName": "exec_command",
"input": { "command": "bash", "args": ["-c", "echo hello | grep hello"] }
"input": {
"command": "bash",
"args": ["-c", "echo hello | grep hello"]
}
},

@@ -414,3 +432,6 @@ "expectedAction": null

"toolName": "exec_command",
"input": { "command": "bash", "args": ["-c", "echo hello > file.txt"] }
"input": {
"command": "bash",
"args": ["-c", "echo hello > file.txt"]
}
},

@@ -431,3 +452,6 @@ "expectedAction": null

"toolName": "exec_command",
"input": { "command": "bash", "args": ["-c", "for i in *.txt; do echo $i; done"] }
"input": {
"command": "bash",
"args": ["-c", "for i in *.txt; do echo $i; done"]
}
},

@@ -480,2 +504,5 @@ "expectedAction": null

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -508,2 +535,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -537,2 +567,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -566,2 +599,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -595,2 +631,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -624,2 +663,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -651,4 +693,7 @@ },

"cache_read_input_tokens": 1,
"cache_creation_input_tokens": 12.50
"cache_creation_input_tokens": 12.5
}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -680,4 +725,7 @@ },

"cache_read_input_tokens": 1,
"cache_creation_input_tokens": 12.50
"cache_creation_input_tokens": 12.5
}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -710,2 +758,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -737,2 +788,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -765,2 +819,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -793,2 +850,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -821,2 +881,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -849,2 +912,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -875,4 +941,7 @@ },

"cache_read_input_tokens": 1,
"cache_creation_input_tokens": 12.50
"cache_creation_input_tokens": 12.5
}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -903,4 +972,7 @@ },

"cache_read_input_tokens": 1,
"cache_creation_input_tokens": 12.50
"cache_creation_input_tokens": 12.5
}
},
"autoCompact": {
"inputTokensKeys": ["input_tokens", "cache_read_input_tokens"]
}

@@ -938,3 +1010,4 @@ },

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -970,3 +1043,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1002,3 +1076,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1034,3 +1109,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1066,3 +1142,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1097,3 +1174,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1128,3 +1206,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1159,3 +1238,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["promptTokenCount"] }
},

@@ -1188,3 +1268,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1216,3 +1297,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1244,3 +1326,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1272,3 +1355,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1300,3 +1384,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1328,3 +1413,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1356,3 +1442,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1383,3 +1470,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1410,3 +1498,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1437,3 +1526,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1464,3 +1554,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1491,3 +1582,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["input_tokens"] }
},

@@ -1514,5 +1606,6 @@

"prompt_tokens_details.cached_tokens": -0.14,
"completion_tokens": 0.6
"completion_tokens": 0.6
}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1541,3 +1634,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1565,3 +1659,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1590,3 +1685,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1607,3 +1703,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1631,3 +1728,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1656,3 +1754,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1672,3 +1771,4 @@ {

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1697,3 +1797,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1722,3 +1823,4 @@

}
}
},
"autoCompact": { "inputTokensKeys": ["prompt_tokens"] }
},

@@ -1745,2 +1847,5 @@

}
},
"autoCompact": {
"inputTokensKeys": ["inputTokens", "cacheReadInputTokens"]
}

@@ -1770,2 +1875,5 @@ },

}
},
"autoCompact": {
"inputTokensKeys": ["inputTokens", "cacheReadInputTokens"]
}

@@ -1772,0 +1880,0 @@ }

{
"name": "@iinm/plain-agent",
"version": "1.13.1",
"version": "1.14.0",
"description": "A lightweight terminal-based coding agent focused on safety and low token cost",

@@ -5,0 +5,0 @@ "license": "MIT",

# Plain Agent
[![CodeQL](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
[![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.13.1)](https://socket.dev/npm/package/@iinm/plain-agent)
[![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.14.0)](https://socket.dev/npm/package/@iinm/plain-agent)
[![install size](https://packagephobia.com/badge?p=@iinm/plain-agent)](https://packagephobia.com/result?p=@iinm/plain-agent)

@@ -232,3 +232,3 @@

- Output truncation — when a command or MCP tool produces large output, it is truncated and saved to a file. The agent can then read only the relevant parts.
- Context compaction — when the context grows large, you can run `/compact` to discard old messages and reload task state from a memory file.
- Context compaction — run `/compact` to discard old messages and reload task state from a memory file. This also happens automatically when input tokens exceed a configurable soft limit.
- MCP tool filtering — MCP servers often expose many tools. Use `enabledTools` in the server config to enable only the ones you need, which reduces the number of tool definitions sent to the model.

@@ -749,2 +749,13 @@

// Auto-compact: when input tokens exceed the soft limit after a tool execution,
// the agent is prompted to update the memory file and call compact_context.
// Reduces noise and token costs before hitting the model's hard limit.
"autoCompact": {
"softLimit": 120000,
// Optional: override per model (prefix match on name+variant)
"softLimitPerModelPrefix": {
"gemini-2.5-pro": 500000
}
},
// Override the default notification command

@@ -751,0 +762,0 @@ "notifyCmd": { "command": "plain-notify-desktop", "args": [] }

@@ -64,2 +64,6 @@ import type { EventEmitter } from "node:events";

initialState?: SessionState | null;
/** Soft limit on input tokens; triggers auto-compact prompt when exceeded. */
contextSoftLimit?: number;
/** Keys in providerTokenUsage to sum for input token count. */
inputTokensKeys?: string[];
};

@@ -37,2 +37,4 @@ /**

initialState,
contextSoftLimit,
inputTokensKeys,
}) {

@@ -199,2 +201,4 @@ /** @type {UserEventEmitter} */

pauseSignal,
contextSoftLimit,
inputTokensKeys,
});

@@ -201,0 +205,0 @@

@@ -10,2 +10,4 @@ /**

import { styleText } from "node:util";
import { buildCompactPrompt } from "./prompt.mjs";
import { extractInputTokenCount } from "./tokenUsage.mjs";
import { compactContextToolName } from "./tools/compactContext.mjs";

@@ -59,2 +61,4 @@

* @property {PauseSignal} pauseSignal - Signal to pause auto-approve after current tool completes
* @property {number} [contextSoftLimit] - Soft limit on input tokens for auto-compact
* @property {string[]} [inputTokensKeys] - Keys in providerTokenUsage to sum for input token count
*/

@@ -79,2 +83,4 @@

pauseSignal,
contextSoftLimit,
inputTokensKeys,
}) {

@@ -108,3 +114,3 @@ const inputHandler = createInputHandler({

const maxThinkingLoops = 5;
let turnsAfterCompactPrompt = -1;
while (true) {

@@ -247,2 +253,3 @@ // Check if auto-approve was paused by Ctrl-C during tool execution

) {
turnsAfterCompactPrompt = -1;
continue;

@@ -262,2 +269,36 @@ }

}
// Auto-compact: insert prompt if context exceeds soft limit
if (contextSoftLimit && inputTokensKeys && providerTokenUsage) {
const inputTokens = extractInputTokenCount(
providerTokenUsage,
inputTokensKeys,
);
if (inputTokens !== undefined && inputTokens > contextSoftLimit) {
if (0 <= turnsAfterCompactPrompt && turnsAfterCompactPrompt < 3) {
turnsAfterCompactPrompt += 1;
} else {
stateManager.appendMessages([
{
role: "user",
content: [
{
type: "text",
text: buildCompactPrompt({
isSubagent: subagentManager.isSubagentActive(),
}),
},
],
},
]);
turnsAfterCompactPrompt = 0;
console.error(
styleText(
"yellow",
`\nContext exceeded soft limit (${inputTokens.toLocaleString()} / ${contextSoftLimit.toLocaleString()} tokens). Auto-compact prompt inserted.`,
),
);
}
}
}
}

@@ -264,0 +305,0 @@ }

@@ -11,3 +11,6 @@ /**

import { loadUserMessageContext } from "../context/loadUserMessageContext.mjs";
import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "../prompt.mjs";
import {
buildCompactPrompt,
CLAUDE_CODE_COMPATIBILITY_NOTES,
} from "../prompt.mjs";
import { parseFileRange } from "../utils/parseFileRange.mjs";

@@ -144,10 +147,6 @@ import { readFileRange } from "../utils/readFileRange.mjs";

if (/^\/compact( |$)/i.test(inputTrimmed)) {
const invocation = inputTrimmed;
const message = [
`System: This prompt was invoked as "${invocation}".`,
"",
"Compact the conversation context:",
"1. Update the memory file for the current task so it fully captures the task overview, progress, decisions, and next steps in a self-contained way.",
'2. Then call the "compact_context" tool alone with that memory file path and a brief reason.',
].join("\n");
const message = buildCompactPrompt({
invocation: inputTrimmed,
isSubagent: agentCommands.getActiveSubagent() !== null,
});
userEventEmitter.emit("userInput", [{ type: "text", text: message }]);

@@ -154,0 +153,0 @@ return "continue";

@@ -90,2 +90,6 @@ import { ClaudeCodePluginRepo } from "./claudeCodePlugin.mjs";

notifyCmd?: { command: string; args?: string[] };
autoCompact?: {
softLimit?: number;
softLimitPerModelPrefix?: Record<string, number>;
};
claudeCodePlugins?: ClaudeCodePluginRepo[];

@@ -92,0 +96,0 @@ };

@@ -127,2 +127,12 @@ /**

},
autoCompact: config.autoCompact
? {
softLimit:
config.autoCompact.softLimit ?? merged.autoCompact?.softLimit,
softLimitPerModelPrefix: {
...(merged.autoCompact?.softLimitPerModelPrefix ?? {}),
...(config.autoCompact.softLimitPerModelPrefix ?? {}),
},
}
: merged.autoCompact,
notifyCmd: config.notifyCmd ?? merged.notifyCmd,

@@ -227,1 +237,19 @@ claudeCodePlugins: [

}
/**
* Resolve the effective context soft limit for the given model.
*
* @param {AppConfig["autoCompact"]} autoCompact
* @param {string} modelNameWithVariant - e.g. "claude-sonnet-4-6+thinking-high"
* @returns {number | undefined}
*/
export function resolveContextSoftLimit(autoCompact, modelNameWithVariant) {
if (!autoCompact) return undefined;
const perPrefix = autoCompact.softLimitPerModelPrefix;
const matched = perPrefix
? Object.entries(perPrefix).find(([prefix]) =>
modelNameWithVariant.startsWith(prefix),
)?.[1]
: undefined;
return matched ?? autoCompact.softLimit ?? undefined;
}

@@ -19,3 +19,3 @@ /**

import { runTestApprovalCommand } from "./cli/testApproval.mjs";
import { loadAppConfig } from "./config.mjs";
import { loadAppConfig, resolveContextSoftLimit } from "./config.mjs";
import { loadAgentRoles } from "./context/loadAgentRoles.mjs";

@@ -432,2 +432,8 @@ import { loadPrompts } from "./context/loadPrompts.mjs";

// Resolve context soft limit from autoCompact config
const contextSoftLimit = resolveContextSoftLimit(
appConfig.autoCompact,
modelNameWithVariant,
);
const { userEventEmitter, agentEventEmitter, agentCommands } = createAgent({

@@ -447,2 +453,4 @@ callModel: agentCallModel,

initialState: resumedState,
contextSoftLimit,
inputTokensKeys: modelDef.autoCompact?.inputTokensKeys,
});

@@ -449,0 +457,0 @@

@@ -13,4 +13,10 @@ import { AnthropicModelConfig } from "./providers/anthropic";

cost?: CostConfig;
autoCompact?: ModelAutoCompactConfig;
};
export type ModelAutoCompactConfig = {
/** Keys in providerTokenUsage whose values are summed to get the input token count. */
inputTokensKeys: string[];
};
export type PlatformConfig =

@@ -17,0 +23,0 @@ | {

@@ -108,1 +108,40 @@ import { toOneLine } from "./utils/toOneLine.mjs";

- If instructed to use "haiku agent", "sonnet agent", or "opus agent", use "worker" instead.`;
/**
* Build the compact context prompt message text.
*
* Shared between the interactive `/compact` command and the automatic
* soft-limit prompt insertion in the agent loop.
*
* @param {object} [options]
* @param {string} [options.invocation] - The invocation string (e.g., "/compact reason").
* When omitted, a generic auto-compact preamble is used instead.
* @param {boolean} [options.isSubagent] - When true, instructs the model to
* return to the main agent via `switch_to_main_agent` before compacting.
* @returns {string}
*/
export function buildCompactPrompt(options) {
const { invocation, isSubagent } = options ?? {};
const preamble = invocation
? `System: This prompt was invoked as "${invocation}".`
: "System: Context is approaching the soft limit.";
if (isSubagent) {
return [
preamble,
"",
"The context is growing large. Wrap up your current work:",
"1. Summarize your progress and findings so far in the memory file.",
'2. Call "switch_to_main_agent" to hand back control to the main agent.',
].join("\n");
}
return [
preamble,
"",
"Compact the conversation context:",
"1. Update the memory file for the current task so it fully captures the task overview, progress, decisions, and next steps in a self-contained way.",
'2. Then call the "compact_context" tool alone with that memory file path and a brief reason.',
].join("\n");
}