Skip to content
Merged
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
10 changes: 2 additions & 8 deletions TERMINOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,8 @@ Canonical words used across Junior's code and documentation.
such as a Slack channel or thread. A Conversation has zero or one Location.
A Run carries this same Location when the agent or tools need it. Location
does not allow output to be sent. Conversation visibility is separate.
- **Delivery**: an optional function that sends Run output to the Conversation
Location. Only Delivery allows output to be sent there. Storing a completed
assistant Message in the Conversation does not depend on Delivery.
- **publish**: whether one Turn also sends assistant output to the Conversation
Location through Delivery. The Conversation always stores each completed
assistant Message. `publishExternally` is the legacy mailbox, Turn checkpoint,
and Run field for this fact until those interfaces use `publish` or Delivery
alone.
- **Delivery**: a function that sends Run output to the Conversation Location.
A Conversation without Delivery stores completed assistant Messages only.
- **User**: one person-level record. A user may have several linked identities.
- **Identity**: one provider account, such as a Slack account in one workspace,
optionally linked to a user.
Expand Down
2 changes: 1 addition & 1 deletion packages/junior/src/api/acp/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function hasConversationAccess(
return Boolean(access?.isParticipant);
}

/** Return whether one Turn can publish its terminal result without racing cleanup. */
/** Return whether one Turn can return its terminal result without racing cleanup. */
async function turnTerminalIsReady(args: {
conversationId: string;
eventStore: ConversationEventStore;
Expand Down
33 changes: 14 additions & 19 deletions packages/junior/src/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ singleton.
- **Destination**: explicit target for output or a side effect. Current uses as
a Conversation Location are migration debt. A feature may use Destination
before it creates a Conversation at that target.
- **publish**: Turn fact that says whether assistant output is also sent to the
Conversation Location through Delivery. The Conversation always stores each
completed assistant Message. `publishExternally` is the legacy field for this
fact.

## Target Interface

Expand Down Expand Up @@ -126,14 +122,12 @@ type InboundMessage = {
source: Source;
actor?: Actor;
input: AgentInput;
publish: boolean;
};

type Turn = {
turnId: string;
source: Source;
actor?: Actor;
publish: boolean;
};

type Delivery = (message: AssistantMessage) => void | Promise<void>;
Expand All @@ -148,11 +142,15 @@ type AgentRun = {
};
```

`Source.kind` states what produced the input. The worker copies Source and
publish from the selected input to the Turn. It loads Location from the
Conversation. Before every new or resumed Run, the provider supplies Delivery
when the Turn publishes. A feature may use Destination to select a target before
it creates a Conversation. That target becomes the new Conversation Location.
`Source.kind` states what produced the input. The worker copies Source from the
selected input to the Turn. It loads Location from the Conversation. Before
every new or resumed Run, the work owner supplies Delivery. Slack input gets
Slack Delivery. Web and local input do not get provider Delivery. Resource
events get Delivery for the Conversation Location. Scheduled, Event task, and
plugin dispatch work gets Delivery for its explicit Destination. Agent
invocation does not get Delivery. A feature may use Destination to select a
target before it creates a Conversation. That target becomes the new
Conversation Location.

Attribution does not grant authority. `run.actors` records participating actors;
credential issuance still requires the current actor or an explicit delegated
Expand Down Expand Up @@ -204,19 +202,16 @@ delegation without becoming the execution actor or a general task owner.
separate.
- The final Run interface has Source, optional Location, and optional Delivery.
Source does not contain Location. Delivery is created for the Location and
does not repeat it. A dashboard continuation may therefore carry a Slack
Location without getting Slack Delivery.
does not repeat it. A dashboard continuation in a Slack Conversation carries
its Location for tools but does not get Slack Delivery.
- The final interface uses Conversation, Source, Location, and Delivery. Do not
add another type, routing object, or wrapper for the same values.
- A Conversation may have one parent Conversation. It stores that relation as
`parentConversationId`. Location is independent and is not copied from the
parent. A Run may read the parent Conversation when it needs that Location.
- Each Turn stores `publish`. The worker gets Source from the selected input and
Location from the Conversation. Before every new or resumed Run, the owning
provider supplies Delivery only when the Turn publishes. Source, Actor,
Destination, and Location do not invent Delivery. The legacy
`publishExternally` field remains only until mailbox and Turn checkpoint data
use the final fact.
- Before each new or resumed Run, the work owner supplies optional Delivery.
Source, Actor, and Location do not select Delivery. A child Conversation does
not get Delivery from its parent's Location.
- Host-owned runtime context and the actor's current instruction are separate
user messages. The context message immediately precedes the instruction,
remains context-authority on resume, and may be replaced before a later model
Expand Down
1 change: 0 additions & 1 deletion packages/junior/src/chat/agent-dispatch/work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ export function buildAgentDispatchInboundMessage(
},
},
receivedAtMs: nowMs,
publishExternally: true,
source: "plugin",
};
}
Expand Down
2 changes: 0 additions & 2 deletions packages/junior/src/chat/agent-invocations/work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export function buildAgentInvocationInboundMessage(
},
},
receivedAtMs: nowMs,
publishExternally: false,
source: "internal",
};
}
Expand Down Expand Up @@ -437,7 +436,6 @@ export function createAgentInvocationWorker(agentRunner: AgentRunner) {
actor: invocation.actor,
credentialContext: invocation.credentialContext,
destination: invocation.destination,
publishExternally: context.publishExternally,
source: invocation.source,
...(location ? { location } : undefined),
surface: "internal",
Expand Down
5 changes: 0 additions & 5 deletions packages/junior/src/chat/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ async function executeAgentRunInPrivacyContext(
slackConversation: run.slackConversation,
slackActionToken: run.slackActionToken,
destination: run.destination,
publishExternally: run.publishExternally,
surface: run.surface,
dispatch: run.dispatch,
toolChannelId: run.toolChannelId,
Expand Down Expand Up @@ -512,10 +511,6 @@ async function executeAgentRunInPrivacyContext(
: undefined),
durability,
recordActiveMcpProviders,
publishExternally:
checkpoint.record?.publishExternally ??
routing.publishExternally ??
false,
actor,
runSource,
conversationId,
Expand Down
2 changes: 0 additions & 2 deletions packages/junior/src/chat/agent/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ interface ResumeStateArgs {
dispatchId?: string;
durability: AgentDurability;
recordActiveMcpProviders: () => Promise<void>;
publishExternally: boolean;
actor?: Actor;
runSource: Source;
conversationId: string;
Expand Down Expand Up @@ -118,7 +117,6 @@ export function createResumeState(args: ResumeStateArgs) {
channelName: args.channelName,
destination: args.destination,
dispatchId: args.dispatchId,
publishExternally: args.publishExternally,
source: args.runSource,
actor: args.actor,
surface: args.surface,
Expand Down
3 changes: 0 additions & 3 deletions packages/junior/src/chat/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,6 @@ export type AgentRun = {
// TODO(dcramer): Remove AgentRun.destination after tool side effects use
// feature-owned targets and place context comes from Location.
destination: Destination;
// TODO(dcramer): Remove AgentRun.publishExternally after Turn checkpoints
// store publish and each provider uses it to supply Delivery before every Run.
publishExternally?: boolean;
surface?: AgentTurnSurface;
dispatch?: AgentDispatch;

Expand Down
4 changes: 2 additions & 2 deletions packages/junior/src/chat/app/conversation-work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from "@/chat/agent-dispatch/store";
import { createSlackRuntime } from "./factory";
import type { JuniorRuntimeServiceOverrides } from "./services";
import { createSlackSystemTurnPublisher } from "@/chat/providers/slack/system-turn";
import { createSlackSystemTurnDelivery } from "@/chat/providers/slack/system-turn";
import {
scheduleSessionCompletedPluginTasks,
type ScheduleSessionCompletedPluginTasksOptions,
Expand Down Expand Up @@ -134,7 +134,7 @@ export function createConversationWork(
const invocationWorker = createAgentInvocationWorker(options.agentRunner);
const conversationTurnWorker = createConversationTurnWorker(
options.agentRunner,
createSlackSystemTurnPublisher({
createSlackSystemTurnDelivery({
getSlackAdapter: options.getSlackAdapter,
state: options.state,
}),
Expand Down
3 changes: 0 additions & 3 deletions packages/junior/src/chat/conversations/web-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,6 @@ export function buildWebInboundMessage(args: {
} satisfies LegacyWebMailboxMetadata,
},
receivedAtMs: nowMs,
// TODO(dcramer): Rename this stored field to publish after deployed
// mailbox readers and writers use the new name.
publishExternally: false,
// TODO(dcramer): Replace this string after deployed mailbox readers and
// writers use a complete web Source.
source: "web",
Expand Down
18 changes: 7 additions & 11 deletions packages/junior/src/chat/providers/slack/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,17 +565,13 @@ async function resumeSlackTurnInContext(
const deliveryState = await getDeliveryConversation();
let slackMessageTs: string[] = [];
try {
// TODO(dcramer): Remove this missing-as-publish fallback after no stored
// Turn checkpoint can omit publishExternally.
if (runArgs.run?.publishExternally !== false) {
slackMessageTs = await sendSlackReply({
channelId: runArgs.channelId,
conversationId: runArgs.conversationId,
replyAttribution: runArgs.run?.dispatch?.replyAttribution,
text,
threadTs: runArgs.threadTs,
});
}
slackMessageTs = await sendSlackReply({
channelId: runArgs.channelId,
conversationId: runArgs.conversationId,
replyAttribution: runArgs.run?.dispatch?.replyAttribution,
text,
threadTs: runArgs.threadTs,
});
} catch (error) {
if (isRetryableSlackPostError(error)) {
throw new RetryableDeliveryError(error);
Expand Down
35 changes: 11 additions & 24 deletions packages/junior/src/chat/providers/slack/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ interface SteeringDrainContext {
export interface SlackTurnOptions extends ReplyHooks {
conversationId?: string;
destination: Destination;
publishExternally?: boolean;
}

const THREAD_OPTOUT_ACK =
Expand Down Expand Up @@ -189,7 +188,6 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
onTurnStatePersisted?: () => Promise<void>;
preparedState?: TPreparedState;
queuedMessages?: QueuedTurnMessage[];
publishExternally?: boolean;
drainSteeringMessages?: (
accept: (messages: QueuedTurnMessage[]) => Promise<void>,
context?: SteeringDrainContext,
Expand Down Expand Up @@ -354,11 +352,6 @@ function actorUserName(message: Message): string | undefined {
}

/** Build the Slack event runtime that routes mentions and subscribed messages. */
/** Slack surfaces publish unless a caller opts out. */
function shouldPublishExternally(publishExternally?: boolean): boolean {
return publishExternally !== false;
}

export function createSlackTurnRuntime<
TPreparedState,
TAssistantEvent extends AssistantLifecycleEvent = AssistantLifecycleEvent,
Expand Down Expand Up @@ -778,7 +771,6 @@ export function createSlackTurnRuntime<
conversationId: hooks.conversationId,
destination: hooks.destination,
queuedMessages,
publishExternally: shouldPublishExternally(hooks.publishExternally),
ack,
onToolInvocation: toolInvocationHook,
onTurnCompleted,
Expand Down Expand Up @@ -843,13 +835,11 @@ export function createSlackTurnRuntime<
lifecycleError = error;
}
await hooks.beforeFirstResponsePost?.();
if (shouldPublishExternally(hooks.publishExternally)) {
await postFallbackErrorReplyWithLogging({
thread,
eventId,
postFailureEventName: "mention.handler.failure_reply_post.failed",
});
}
await postFallbackErrorReplyWithLogging({
thread,
eventId,
postFailureEventName: "mention.handler.failure_reply_post.failed",
});
if (lifecycleError) throw lifecycleError;
} finally {
if (completed) {
Expand Down Expand Up @@ -1111,7 +1101,6 @@ export function createSlackTurnRuntime<
conversationId: hooks.conversationId,
destination: hooks.destination,
preparedState,
publishExternally: shouldPublishExternally(hooks.publishExternally),
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
queuedMessages,
ack,
Expand Down Expand Up @@ -1178,14 +1167,12 @@ export function createSlackTurnRuntime<
lifecycleError = error;
}
await hooks.beforeFirstResponsePost?.();
if (shouldPublishExternally(hooks.publishExternally)) {
await postFallbackErrorReplyWithLogging({
thread,
eventId,
postFailureEventName:
"subscribed_message.handler.failure_reply_post.failed",
});
}
await postFallbackErrorReplyWithLogging({
thread,
eventId,
postFailureEventName:
"subscribed_message.handler.failure_reply_post.failed",
});
if (lifecycleError) throw lifecycleError;
} finally {
if (completed) {
Expand Down
15 changes: 10 additions & 5 deletions packages/junior/src/chat/providers/slack/system-turn.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type { SlackAdapter } from "@chat-adapter/slack";
import type { StateAdapter } from "chat";
import type { PublishMessage } from "@/chat/task-execution/assistant-message";
import type { DeliverMessage } from "@/chat/task-execution/assistant-message";
import { RetryableDeliveryError } from "@/chat/agent/types";
import { runWithSlackInstallation } from "@/chat/slack/adapter-context";
import { isRetryableSlackPostError } from "@/chat/slack/errors";
import { sendSlackReply } from "@/chat/slack/reply";

/** Create Slack publishing for system Turns that have no webhook Message. */
export function createSlackSystemTurnPublisher(args: {
/**
* Deliver system Turn output to Slack without a webhook Message.
*
* TODO(dcramer): Replace this Location-taking function with Delivery bound to one
* Location after Resource event work supplies Delivery before the Run.
*/
export function createSlackSystemTurnDelivery(args: {
getSlackAdapter: () => SlackAdapter;
state?: StateAdapter;
}): PublishMessage {
}): DeliverMessage {
return async ({ conversationId, location, text }) => {
if (location.provider !== "slack") {
throw new Error(
`Slack system Turn cannot publish to ${location.provider} Location`,
`Slack system Turn cannot deliver to ${location.provider} Location`,
);
}
let messageIds: string[];
Expand Down
Loading
Loading