Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* LLM interface module entry point.
|
||||
*
|
||||
* Exports `GenerativeModel` (the LLMInterface implementation), along with internal
|
||||
* pure conversion functions for unit testing (message merging, event translation,
|
||||
* token accounting, UniConfig construction, retry determination).
|
||||
*/
|
||||
export {
|
||||
GenerativeModel,
|
||||
EventTranslator,
|
||||
groupHistoryToUniMessages,
|
||||
mergeOmniToUniMessage,
|
||||
translateEvents,
|
||||
usageToTokenCounts,
|
||||
isMalformedJsonParseError,
|
||||
isIncompleteStreamError,
|
||||
isRetryableError,
|
||||
mapThinkingLevel,
|
||||
toolDefinitionsToSchemas,
|
||||
buildUniConfig,
|
||||
} from "./generative-model.js";
|
||||
export { ToolCallIdAllocator, stripToolCallIdSuffix } from "./tool-call-ids.js";
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Session-level uniqueness for tool_call_id.
|
||||
*
|
||||
* Some providers don't produce a real call id: e.g. Gemini's functionCall has no id, so AgentHub
|
||||
* uses the **function name** as the `tool_call_id` — consecutive/parallel calls to the same tool then
|
||||
* all share one id. But the OmniMessage world (engine dispatch/pairing, approval routing, frontend
|
||||
* tool-card attribution) keys on `tool_call_id`, and a collision lets a later call overwrite the
|
||||
* earlier one (parallel same-name calls in one turn can even be dropped entirely).
|
||||
*
|
||||
* Approach: inbound, `EventTranslator` disambiguates duplicate ids with a `#n` suffix (the first keeps
|
||||
* the original id); outbound (returning tool_result, replaying history on resume) uses
|
||||
* `stripToolCallIdSuffix` to strip the suffix and restore the original — Gemini's functionResponse
|
||||
* pairs by using `tool_call_id` as the name, so it must be restored to the function name. The registry
|
||||
* lives at Session level (the new GenerativeModel rebuilt on compaction shares the same instance), and
|
||||
* on resume `setHistory` seeds it with historical ids, so the uniqueness scope covers the entire
|
||||
* context the frontend renders.
|
||||
* Docs: /docs/interfaces § "The built-in implementation: GenerativeModel".
|
||||
*/
|
||||
export class ToolCallIdAllocator {
|
||||
/** OmniMessage-level tool_call_ids already taken in this Session (history-seeded + allocated). */
|
||||
private used = new Set<string>();
|
||||
|
||||
/** Register an already-used id (for resume seeding); registering twice is harmless. */
|
||||
markUsed(id: string): void {
|
||||
this.used.add(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a Session-unique id for a provider-reported tool_call_id: if unused, keep the original;
|
||||
* if already used (a repeat call from a name-as-id provider), take the first free `origId#n` (n from 2).
|
||||
* Providers with truly unique ids (OpenAI `call_*` / Claude `toolu_*`) never collide, so they pass through unchanged.
|
||||
*/
|
||||
allocate(providerId: string): string {
|
||||
let id = providerId;
|
||||
for (let n = 2; this.used.has(id); n += 1) {
|
||||
id = `${providerId}#${n}`;
|
||||
}
|
||||
this.used.add(id);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the `#n` suffix added by `allocate`, restoring the provider's original id (returns as-is when
|
||||
* there's no suffix; idempotent). On resume there's no registry to compare against, so it trims by
|
||||
* shape: real ids from known providers (OpenAI/Claude `call_*`/`toolu_*`, Gemini function names — `#`
|
||||
* isn't a valid function-name char) never end in `#<digits>`, so they aren't harmed.
|
||||
*/
|
||||
export function stripToolCallIdSuffix(id: string): string {
|
||||
return id.replace(/#\d+$/, "");
|
||||
}
|
||||
Reference in New Issue
Block a user