Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e87680a
feat(catalog): add GitLab Duo Agent model discovery
jiwangyihao Jun 19, 2026
3126282
feat(ai): add GitLab Duo Agent provider
jiwangyihao Jun 19, 2026
a4577f4
feat(agent): thread cwd through to local tool execution
jiwangyihao Jun 19, 2026
9d74c66
chore: ignore .worktree directory
jiwangyihao Jun 19, 2026
e222239
feat(ai): flatten Duo Agent goal transcript and harden tool-loop edges
jiwangyihao Jun 20, 2026
4fad098
fix(ai): stop stranded Duo Agent workflow on unresolvable tool batch
jiwangyihao Jun 21, 2026
16e9864
refactor(ai): drop Duo Agent tool-call batching for serial dispatch
jiwangyihao Jun 21, 2026
ae76e54
feat(ai): cache Duo Agent namespace discovery per account
jiwangyihao Jun 21, 2026
73bf512
fix(ai): make Duo Agent account preparation account-scoped
jiwangyihao Jun 21, 2026
39a9579
fix(ai): address Codex review for Duo Agent provider
jiwangyihao Jun 22, 2026
75318c7
test(ai): 修复 Duo Agent 命名空间缓存测试的 socket 等待时序
jiwangyihao Jun 22, 2026
b153706
fix(ai): 处理 rebase 后 Codex 新增的三项审阅意见
jiwangyihao Jun 22, 2026
0572f8b
fix(catalog): 处理 Codex 对 Duo 模型缓存与远程端口比较的两项审阅意见
jiwangyihao Jun 22, 2026
feb1ead
fix(ai): 处理 Codex 对 resume socket 停止与 changelog 归属的两项审阅意见
jiwangyihao Jun 22, 2026
a5003d9
fix(ai): 处理 Codex 对项目自动发现、设置启用与 changelog 归属的三项审阅意见
jiwangyihao Jun 22, 2026
8211cc8
fix(catalog): 将 GitLab Duo Agent fallback 模型纳入 bundled models.json
jiwangyihao Jun 23, 2026
23092db
fix(gitlab-duo): direct_access 错误保留 HTTP 状态码以触发凭据轮换
jiwangyihao Jun 23, 2026
a088037
fix(agent): /move 后按会话实时 cwd 重新作用域 Duo 发现
jiwangyihao Jun 23, 2026
61755d4
fix(ai): 仅为 paste-code provider 合成默认手动粘贴码提示
jiwangyihao Jun 23, 2026
b7ab4b6
fix(gitlab-duo): checkpoint 去重按回合位置作用域,避免吞掉重复文本的新消息
jiwangyihao Jun 23, 2026
ec5581a
feat(gitlab-duo): shrink goal transcript and bound oversized goals as…
jiwangyihao Jun 23, 2026
4846cff
fix(gitlab-duo): register MCP tools under bare names
jiwangyihao Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ packages/ai/test/.temp-images/
.pi_config/
.opencode/
.worktrees/
.worktree/
compaction-results/
changes/
__pycache__/
Expand Down
9 changes: 8 additions & 1 deletion packages/agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Added an optional `cwdResolver` to `Agent` (and a `getCwd` per-call resolver on `AgentLoopConfig`) that is read once per LLM call to resolve the working directory, overriding the static `cwd` (falling back to it when the resolver returns `undefined`). Lets a host reflect a session move into provider options without reconstructing the agent — workspace-scoped provider discovery (e.g. GitLab Duo Agent namespace/project) now follows the live directory instead of the directory captured at construction.

## [16.1.16] - 2026-06-23

### Added
Expand All @@ -13,6 +17,10 @@

- Updated `buildSideRequestContext` to allow pinning custom system prompts

### Fixed

- Fixed `Agent` forwarding the working directory (`cwd`) into provider stream options so the GitLab Duo Agent provider can scope local tool execution to the workspace.

## [16.1.10] - 2026-06-21

### Fixed
Expand Down Expand Up @@ -131,7 +139,6 @@
### Fixed

- Fixed `pruneToolOutputs` blanking tiny tool results during overflow pruning: results below `50` tokens (`MIN_PRUNE_TOKENS`) are no longer replaced with the `[Output truncated - N tokens]` placeholder, which cost more tokens than the result itself and churned the prompt cache for zero savings.

## [15.13.2] - 2026-06-15

### Breaking Changes
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,9 @@ async function streamAssistantResponse(
const effectiveToolChoice = ownedDialect ? undefined : (hostToolChoice ?? forcedToolChoice ?? config.toolChoice);
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
const effectiveDisableReasoning = dynamicDisableReasoning ?? config.disableReasoning;
// `getCwd` is read once per LLM call so a mid-run session move (`/move`) reaches
// workspace-scoped provider discovery; falls back to the static `cwd` when unset.
const effectiveCwd = config.getCwd?.() ?? config.cwd;

const chatStepNumber = stepCounter.count;
stepCounter.count += 1;
Expand Down Expand Up @@ -1272,6 +1275,7 @@ async function streamAssistantResponse(
disableReasoning: effectiveDisableReasoning,
temperature: effectiveTemperature,
serviceTier: effectiveServiceTier,
cwd: effectiveCwd,
signal: finalRequestSignal,
onResponse: captureOnResponse,
});
Expand Down
18 changes: 18 additions & 0 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ export interface AgentOptions {
*/
cursorOnToolResult?: CursorToolResultHandler;

/** Current working directory used by local tool execution. */
cwd?: string;
/**
* Resolver for the live working directory, re-read on every turn. When set, it
* overrides the static {@link cwd} at config-build time so a session move
* (`/move`, which updates the host's cwd without reconstructing the Agent) is
* reflected in provider options — e.g. GitLab Duo Agent namespace/project
* discovery keys off this cwd's git remote. Falls back to `cwd` when it returns
* `undefined`.
*/
cwdResolver?: () => string | undefined;
/**
* Called after a tool call has been validated and is about to execute.
* See {@link AgentLoopConfig.beforeToolCall} for full semantics.
Expand Down Expand Up @@ -354,6 +365,9 @@ export class Agent {
#getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
#cursorExecHandlers?: CursorExecHandlers;
#cursorOnToolResult?: CursorToolResultHandler;
#cwd?: string;
#cwdResolver?: () => string | undefined;

#runningPrompt?: Promise<void>;
#resolveRunningPrompt?: () => void;
#kimiApiFormat?: "openai" | "anthropic";
Expand Down Expand Up @@ -429,6 +443,8 @@ export class Agent {
this.#getToolContext = opts.getToolContext;
this.#cursorExecHandlers = opts.cursorExecHandlers;
this.#cursorOnToolResult = opts.cursorOnToolResult;
this.#cwd = opts.cwd;
this.#cwdResolver = opts.cwdResolver;
this.#kimiApiFormat = opts.kimiApiFormat;
this.#preferWebsockets = opts.preferWebsockets;
this.#transformToolCallArguments = opts.transformToolCallArguments;
Expand Down Expand Up @@ -1129,6 +1145,8 @@ export class Agent {
},
cursorExecHandlers: this.#cursorExecHandlers,
cursorOnToolResult,
cwd: this.#cwd,
Comment thread
jiwangyihao marked this conversation as resolved.
getCwd: this.#cwdResolver,
transformToolCallArguments: this.#transformToolCallArguments,
intentTracing: this.#intentTracing,
pruneToolDescriptions: this.#pruneToolDescriptions,
Expand Down
11 changes: 11 additions & 0 deletions packages/agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,17 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
*/
getServiceTier?: (model: Model) => ServiceTier | undefined;

/**
* Per-call working-directory resolver, read once per LLM call. When set, its
* return value overrides the static {@link SimpleStreamOptions.cwd} for the
* request (falling back to that static `cwd` when it returns `undefined`).
* Lets the host reflect a session move (`/move`, which updates the working
* directory without reconstructing the loop config) into provider options —
* e.g. GitLab Duo Agent namespace/project discovery keys off this cwd's git
* remote, so a stale value would strand discovery on the original repo.
*/
getCwd?: () => string | undefined;

/**
* Called after a tool call has been validated and is about to execute.
*
Expand Down
72 changes: 72 additions & 0 deletions packages/agent/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,78 @@ describe("Agent", () => {
expect(mock.calls[0]?.options?.promptCacheKey).toBe("parent-cache");
});

it("forwards the live cwd from cwdResolver to the stream, overriding the static cwd", async () => {
const mock = createMockModel({ responses: [{ content: ["ok"] }] });
const agent = new Agent({
initialState: { model: mock.model, messages: [] },
streamFn: mock.stream,
cwd: "/static/repo-a",
cwdResolver: () => "/live/repo-b",
});

await agent.prompt("run");

// The resolver wins over the constructor-time `cwd`: provider workspace
// discovery (e.g. GitLab Duo namespace/project) must key off the live dir.
expect(mock.calls[0]?.options?.cwd).toBe("/live/repo-b");
});

it("falls back to the static cwd when cwdResolver returns undefined", async () => {
const mock = createMockModel({ responses: [{ content: ["ok"] }] });
const agent = new Agent({
initialState: { model: mock.model, messages: [] },
streamFn: mock.stream,
cwd: "/static/repo-a",
cwdResolver: () => undefined,
});

await agent.prompt("run");

expect(mock.calls[0]?.options?.cwd).toBe("/static/repo-a");
});

it("re-reads cwd from cwdResolver for each model call within a run (a /move mid-run is seen)", async () => {
const toolSchema = z.object({ value: z.string() });
type Details = { value: string };
const alphaTool: AgentTool<typeof toolSchema, Details> = {
name: "alpha",
label: "Alpha",
description: "Alpha tool",
parameters: toolSchema,
async execute(_toolCallId, params) {
return { content: [{ type: "text", text: `alpha:${params.value}` }], details: { value: params.value } };
},
};

const mock = createMockModel({
responses: [
{ content: [{ type: "toolCall", id: "tool-1", name: "alpha", arguments: { value: "hello" } }] },
{ content: ["done"] },
],
});

// The host owns the live cwd; `cwdResolver` reads it on every config build.
let liveCwd = "/live/repo-a";
const agent = new Agent({
initialState: { model: mock.model, tools: [alphaTool], messages: [] },
streamFn: mock.stream,
cwdResolver: () => liveCwd,
});

// Simulate `/move` between the tool-call turn and the continuation request.
const unsubscribe = agent.subscribe(event => {
if (event.type === "message_end" && event.message.role === "toolResult") {
liveCwd = "/live/repo-b";
}
});

await agent.prompt("run");
unsubscribe();

const cwdPerCall = mock.calls.map(call => call.options?.cwd);
expect(cwdPerCall).toEqual(["/live/repo-a", "/live/repo-b"]);
});

it("returns static metadata via the plain setter", () => {
const agent = new Agent();
expect(agent.metadata).toBeUndefined();
Expand Down
Loading