Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 2 additions & 5 deletions docs/src/content/docs/agent-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,7 @@ Open an agent and use the **chat** surface to probe its behavior against the
live **draft** — chat always runs the draft, not stable. When something needs to
change, submit a change request from the chat thread. TAS opens a PR via Tembo
(same as new-agent authoring). Review and merge; the draft updates on your
default branch. Later change requests for that agent continue its existing Tembo
session, so the coding agent retains context from the earlier changes and can
resume its workspace instead of starting from scratch. If that session is no
longer available, TAS starts a new one automatically.
default branch.

This is separate from **Improve the Agent** on a run, which anchors feedback to a
specific execution — see [Improvements](/agent-studio/improvements/).
Expand All @@ -97,4 +94,4 @@ writes the file back with a new commit.
**Settings → Danger → Delete workspace** removes all workspace data (members,
runs, schedules, connections, secrets, audit) but does **not** touch your
GitHub repository or its agent files. Admin-only, type-to-confirm. See
[Settings](/agent-studio/settings/).
[Settings](/agent-studio/settings/).
5 changes: 0 additions & 5 deletions docs/src/content/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ they are no longer release versions. Phase scope now lives in
instant and local date/time fields for a requested IANA timezone, giving
scheduled agents a reliable basis for relative windows and date-based dedup.

### Fixed
- **Agent changes reuse their Tembo coding session.** Later edits to the same
agent now continue the latest session, retaining prior change context and a
warm workspace. Missing old sessions fall back to creating a new one.

## v2026.7.4 — Inbox reading view for digests, markdown context + run links, quieter Slack threads

### Added
Expand Down
10 changes: 2 additions & 8 deletions web/src/app/[workspace]/agents/[agent]/chat/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import {
} from "@/lib/auth-server";
import {
buildChatEditPrompt,
dispatchTemboTask,
createTemboTask,
type CapError,
} from "@/lib/cap-api";
import {
createImprovement,
getLatestTemboTaskForAgent,
improvementMarker,
setImprovementCommitted,
setImprovementTask,
Expand Down Expand Up @@ -65,10 +64,6 @@ export async function chatSubmitAction(args: {
};
}
const canonicalName = agent.spec.name;
const existingTask = await getLatestTemboTaskForAgent(
workspace.id,
canonicalName,
);

const repo = await getWorkspaceRepo(workspace.id);
if (!repo) {
Expand Down Expand Up @@ -114,9 +109,8 @@ export async function chatSubmitAction(args: {
)),
});

const res = await dispatchTemboTask({
const res = await createTemboTask({
apiKey: temboCredential.apiKey,
existingTask,
input: {
prompt,
repositoryUrl: `https://github.com/${repo.owner}/${repo.name}`,
Expand Down
10 changes: 2 additions & 8 deletions web/src/app/[workspace]/agents/[agent]/runs/[runId]/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import {
} from "@/lib/auth-server";
import {
buildImprovePrompt,
dispatchTemboTask,
createTemboTask,
type CapError,
} from "@/lib/cap-api";
import {
createImprovement,
getLatestTemboTaskForAgent,
improvementMarker,
setImprovementCommitted,
setImprovementTask,
Expand Down Expand Up @@ -52,10 +51,6 @@ export async function improveAgentAction(args: {

const run = await getRun(args.runId, workspace.id);
if (!run || run.workspaceId !== workspace.id) notFound();
const existingTask = await getLatestTemboTaskForAgent(
workspace.id,
run.agentName,
);

const repo = await getWorkspaceRepo(workspace.id);
if (!repo) {
Expand Down Expand Up @@ -99,9 +94,8 @@ export async function improveAgentAction(args: {
defaultBranch: repo.defaultBranch,
});

const res = await dispatchTemboTask({
const res = await createTemboTask({
apiKey: temboCredential.apiKey,
existingTask,
input: {
prompt,
repositoryUrl: `https://github.com/${repo.owner}/${repo.name}`,
Expand Down
12 changes: 2 additions & 10 deletions web/src/lib/api-v1/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { lookupUserByEmail, postMessage } from "@/lib/slack-api";
import {
buildChatEditPrompt,
buildCreateAgentPrompt,
dispatchTemboTask,
createTemboTask,
} from "@/lib/cap-api";
import { resolveTemboCredential } from "@/lib/tembo-credentials";
import { buildPromptConnectionContext } from "@/lib/prompt-connections";
Expand All @@ -36,7 +36,6 @@ import {
import { validateCron } from "@/lib/cron";
import {
createImprovement,
getLatestTemboTaskForAgent,
improvementMarker,
setImprovementCommitted,
setImprovementTask,
Expand Down Expand Up @@ -346,10 +345,6 @@ export async function requestAgentChange(
kind = "edit";
agentName = found.agent.spec.name;
agentPath = found.agent.path;
const existingTask = await getLatestTemboTaskForAgent(
ctx.workspace.id,
agentName,
);
const row = await createImprovement({
workspaceId: ctx.workspace.id,
runId: null,
Expand Down Expand Up @@ -381,7 +376,6 @@ export async function requestAgentChange(
prompt,
kind,
agentPath,
existingTask,
});
}

Expand Down Expand Up @@ -464,11 +458,9 @@ async function finishTask(args: {
prompt: string;
kind: "edit" | "create";
agentPath: string;
existingTask?: { taskId: string; htmlUrl: string } | null;
}): Promise<{ ok: true; result: RequestAgentChangeResult } | ActionFailure> {
const res = await dispatchTemboTask({
const res = await createTemboTask({
apiKey: args.apiKey,
existingTask: args.existingTask,
input: {
prompt: args.prompt,
repositoryUrl: args.repositoryUrl,
Expand Down
120 changes: 5 additions & 115 deletions web/src/lib/cap-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import { dispatchTemboTask, validateTemboApiKey } from "@/lib/cap-api";
import { validateTemboApiKey } from "./cap-api";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops the last test that proves createTemboTask posts to /public-api/session/create with the expected payload, but that helper is still the shared path for new-agent, chat-edit, run-improve, and API requests. Can we keep a small create-path test here so future endpoint/payload regressions are caught without reintroducing the deleted resume behavior?


const input = {
prompt: "Update the agent instructions",
repositoryUrl: "https://github.com/tembo/example",
targetBranch: "main",
};

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

describe("dispatchTemboTask", () => {
beforeEach(() => {
vi.stubEnv("TEMBO_API_URL", "https://api.example.test");
vi.spyOn(console, "log").mockImplementation(() => undefined);
});

it("creates a new session when there is no prior agent task", async () => {
const fetchMock = vi.fn().mockResolvedValue(
Response.json({
id: "new-session",
title: "Update agent",
status: "queued",
htmlUrl: "https://app.tembo.io/sessions/new-session",
}),
);
vi.stubGlobal("fetch", fetchMock);

const result = await dispatchTemboTask({ apiKey: "secret", input });

expect(result).toEqual({
ok: true,
result: {
taskId: "new-session",
title: "Update agent",
status: "queued",
htmlUrl: "https://app.tembo.io/sessions/new-session",
},
});
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0]?.[0]).toBe(
"https://api.example.test/public-api/session/create",
);
});

it("submits a follow-up to the existing agent session", async () => {
const fetchMock = vi.fn().mockResolvedValue(
Response.json({
message: { isQueued: true },
session: { id: "existing-session", title: "Update agent" },
}),
);
vi.stubGlobal("fetch", fetchMock);

const result = await dispatchTemboTask({
apiKey: "secret",
input,
existingTask: {
taskId: "existing-session",
htmlUrl: "https://app.tembo.io/sessions/existing-session",
},
});

expect(result).toEqual({
ok: true,
result: {
taskId: "existing-session",
title: "Update agent",
status: "queued",
htmlUrl: "https://app.tembo.io/sessions/existing-session",
},
});
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0]?.[0]).toBe(
"https://api.example.test/session/existing-session/message",
);
expect(JSON.parse(fetchMock.mock.calls[0]?.[1]?.body as string)).toEqual({
content: input.prompt,
});
});

it("creates a new session when the prior Tembo session no longer exists", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response("missing", { status: 404 }))
.mockResolvedValueOnce(
Response.json({
id: "replacement-session",
title: "Update agent",
status: "queued",
htmlUrl: "https://app.tembo.io/sessions/replacement-session",
}),
);
vi.stubGlobal("fetch", fetchMock);

const result = await dispatchTemboTask({
apiKey: "secret",
input,
existingTask: {
taskId: "missing-session",
htmlUrl: "https://app.tembo.io/sessions/missing-session",
},
});

expect(result.ok && result.result.taskId).toBe("replacement-session");
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
"https://api.example.test/session/missing-session/message",
"https://api.example.test/public-api/session/create",
]);
describe("validateTemboApiKey", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
});

describe("validateTemboApiKey", () => {
it("returns the Tembo account identity for a valid key", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ userId: "user-1", orgId: "org-1" }), {
Expand Down
Loading
Loading