Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5d9f11a
workstream: agent pages stay the agent's, lookup by agent, markdown i…
AbirAbbas Nov 3, 2026
1df6203
session: a workstream agent's prompt section and six tools behind a door
AbirAbbas Nov 3, 2026
01f0e35
app: give a workstream's agent its door, and re-read spend after its …
AbirAbbas Nov 3, 2026
032c457
manual: what a workstream's agent can do
AbirAbbas Nov 3, 2026
80dfd1e
docs: change entry for the workstream agent's tools
AbirAbbas Nov 3, 2026
11c4168
app: gofmt the workstream agent door test
AbirAbbas Nov 3, 2026
ba35a47
workstream: agent writes check done in their transaction; markdown im…
AbirAbbas Nov 3, 2026
47e9044
session: workstream tools follow the argument and retry laws
AbirAbbas Nov 3, 2026
6f05c64
app: keep each workstream agent's brief current without reading the s…
AbirAbbas Nov 3, 2026
41b87cb
docs: workstream agent tools refuse repeated keys; done is checked in…
AbirAbbas Nov 3, 2026
bd497d9
session: import the workstream types in the validation test
AbirAbbas Nov 3, 2026
32a2f86
workstream: only an explicitly attributed agent proposal is refused w…
AbirAbbas Nov 3, 2026
ec4d2a4
session: the workstream agent keeps its "Where it stands" page current
AbirAbbas Nov 3, 2026
d8b24ab
session: a shorter Where it stands rule, and room for it in the promp…
AbirAbbas Nov 3, 2026
9a76708
tests: the conformance seed lends the agent the Overview for its one …
AbirAbbas Nov 3, 2026
f4ac14a
tests: page version and restore fixtures write as the agent only on i…
AbirAbbas Nov 3, 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
6 changes: 5 additions & 1 deletion cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func runApp(args []string) error {
// ONE SUPERVISOR FOR EVERY CONVERSATION THE APP BUILDS (appservices.go).
services := openAppServices()
appServiceDoor.Store(services)
appWorkstreamAgents.Store(backend)
backend.attachServices(services)
guard.Go("app/crews-start", func() { backend.crews() })
// THE APP IS AN OPEN WINDOW for as long as it runs, so the clock that runs
Expand Down Expand Up @@ -2981,7 +2982,10 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
if held {
live.box.Event(appwire.EventAgentChanged, appwire.AgentChanged{Agent: view})
}
b.spendMoved(func(spend appwire.Spend) { b.moneyAfter(a, live, spend.TodayUSD) })
b.spendMoved(func(spend appwire.Spend) {
b.moneyAfter(a, live, spend.TodayUSD)
b.workstreamAgentTurnSettled(id)
})
}

// ── entry ids ───────────────────────────────────────────────────────────────
Expand Down
109 changes: 67 additions & 42 deletions cmd/codeaf/appworkstreams.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type appWorkstreams struct {
cacheMu sync.Mutex
cache map[string]workstreamQueueRead
flights map[string]chan struct{}
briefMu sync.Mutex
briefs map[string]session.WorkstreamBrief
}

type workstreamQueueRead struct {
Expand Down Expand Up @@ -98,6 +100,8 @@ func appWorkstreamError(err error) error {
return appwire.Errorf(appwire.CodeCycle, "%s", err)
case errors.Is(err, workstream.ErrTooLarge):
return appwire.Errorf(appwire.CodeTooLarge, "%s", err)
case errors.Is(err, workstream.ErrNotAgentPage):
return appwire.Errorf(appwire.CodeBadParams, "%s", err)
}
return appCollectionError(err)
}
Expand Down Expand Up @@ -175,6 +179,7 @@ func (b *appBackend) WorkstreamList(p appwire.WorkstreamListParams) (appwire.Wor
}
jobs := []workstream.Job{}
for _, w := range rows {
b.workstreamRememberBrief(w)
jobs = append(jobs, w.Jobs()...)
}
facts := b.workstreamFacts(jobs)
Expand All @@ -185,6 +190,7 @@ func (b *appBackend) WorkstreamList(p appwire.WorkstreamListParams) (appwire.Wor
if err != nil {
return appwire.WorkstreamListResult{}, err
}
b.workstreamRememberBrief(w)
v := appWorkstream(w, facts, b.workstreamSpend(w.AgentID))
out.Workstreams = append(out.Workstreams, appwire.WorkstreamRow{ID: v.ID, Name: v.Name, Stage: v.Stage, Total: v.Total, Merged: v.Merged, NeedsYou: v.NeedsYou, SpendUSD: v.SpendUSD, BudgetUSD: v.BudgetUSD, UpdatedAt: v.UpdatedAt})
}
Expand All @@ -197,6 +203,7 @@ func (b *appBackend) WorkstreamGet(p appwire.WorkstreamGetParams) (appwire.Works
if err != nil {
return appwire.WorkstreamGetResult{}, workstream.Change{}, err
}
b.workstreamRememberBrief(w)
facts := b.workstreamFacts(w.Jobs())
w, c, err := b.workstreamObserved(s, w, facts, workstream.Change{})
return appwire.WorkstreamGetResult{Workstream: appWorkstream(w, facts, b.workstreamSpend(w.AgentID))}, c, err
Expand All @@ -206,8 +213,8 @@ func (b *appBackend) WorkstreamGet(p appwire.WorkstreamGetParams) (appwire.Works
// workstreamAgentAsk is the single agent-creation hook for workstreams. Empty
// text is a bare open in prepareCreate and cannot start a model turn.
func workstreamAgentAsk(p appwire.WorkstreamCreateParams) appwire.CreateAsk {
// WP-10 attaches the workstream system prompt and tools here.
return appwire.CreateAsk{Model: workstreamValue(p.Model), CreatedBy: "workstream"}
// Engine assembly attaches the prompt and tools through appWorkstreamAgentDoor.
return appwire.CreateAsk{Model: workstreamValue(p.Model), CreatedBy: workstreamCreatedBy}
}

// workstreamPreflight checks store-owned refusals without writing or migrating.
Expand Down Expand Up @@ -287,32 +294,16 @@ func (b *appBackend) WorkstreamCreate(p appwire.WorkstreamCreateParams) (appwire
if existing != nil && workstreamValue(p.ClientID) != "" {
w, err := existing.ByClientID(context.Background(), *p.ClientID)
if err == nil {
b.workstreamRememberBrief(w)
return appwire.WorkstreamCreateResult{Workstream: appWorkstream(w, nil, b.workstreamSpend(w.AgentID))}, nil
}
if !errors.Is(err, workstream.ErrNotFound) {
return zero, appWorkstreamError(err)
}
}
if p.OriginChatID != nil {
if _, err := b.find(*p.OriginChatID); err != nil {
return zero, err
}
}
store, err := b.collectionStore(false)
if err != nil {
if err := b.workstreamCreatePreflight(p); err != nil {
return zero, err
}
if store != nil {
if err := workstreamPreflight(store, p); err != nil {
return zero, appWorkstreamError(err)
}
} else {
for _, ref := range p.Refs {
if ref.Kind == appwire.RefCollection {
return zero, appCollectionError(workspace.ErrNotFound)
}
}
}
create := b.workstreams.createAgent
if create == nil {
create = b.CreateAgent
Expand All @@ -321,43 +312,74 @@ func (b *appBackend) WorkstreamCreate(p appwire.WorkstreamCreateParams) (appwire
if err != nil {
return zero, err
}
w, change, err := b.workstreamCreateWithAgent(p, agent)
if err != nil {
return zero, appWorkstreamError(err)
}
b.workstreamRememberBrief(w)
b.workstreamChanged(change)
if change.ID != "" {
b.collectionChanged(w.ID, false)
}
return appwire.WorkstreamCreateResult{Workstream: appWorkstream(w, nil, b.workstreamSpend(w.AgentID))}, nil
}

func (b *appBackend) workstreamCreatePreflight(p appwire.WorkstreamCreateParams) error {
if p.OriginChatID != nil {
if _, err := b.find(*p.OriginChatID); err != nil {
return err
}
}
store, err := b.collectionStore(false)
if err != nil {
return err
}
if store != nil {
return appWorkstreamError(workstreamPreflight(store, p))
}
for _, ref := range p.Refs {
if ref.Kind == appwire.RefCollection {
return appCollectionError(workspace.ErrNotFound)
}
}
return nil
}

func (b *appBackend) workstreamCreateWithAgent(p appwire.WorkstreamCreateParams, agent appwire.Agent) (workstream.Workstream, workstream.Change, error) {
refs := make([]workspace.Ref, 0, len(p.Refs))
for _, r := range p.Refs {
refs = append(refs, workspace.Ref{Kind: workspace.Kind(r.Kind), ID: r.ID, SessionID: workstreamValue(r.SessionID), Factory: workstreamValue(r.Factory)})
}
var w workstream.Workstream
var change workstream.Change
store, err = b.collectionStore(true)
store, err := b.collectionStore(true)
if err == nil {
s := workstream.New(store, workstream.Options{Now: b.now})
w, change, err = s.Create(context.Background(), workstream.Create{Name: strings.TrimSpace(p.Name), Why: workstreamValue(p.Why), OriginChatID: workstreamValue(p.OriginChatID), Refs: refs, AgentID: agent.ID, ClientID: workstreamValue(p.ClientID)})
}
if err != nil || w.AgentID != agent.ID {
closeAgent := b.workstreams.closeAgent
if closeAgent == nil {
closeAgent = func(id string) error { return b.MoveToClosed([]string{id}) }
}
if cleanup := closeAgent(agent.ID); cleanup != nil {
if err != nil {
mapped := appWorkstreamError(err)
var wire *appwire.Error
if errors.As(mapped, &wire) {
copy := *wire
copy.Message += "; the unused workstream agent could not be closed"
return zero, &copy
}
}
return zero, appwire.Errorf(appwire.CodeEngine, "the unused workstream agent could not be closed: %s", appUnaskedReason("the conversation", cleanup))
if cleanup := b.workstreamCloseUnusedAgent(agent.ID, err); cleanup != nil {
return workstream.Workstream{}, workstream.Change{}, cleanup
}
}
if err != nil {
return zero, appWorkstreamError(err)
return w, change, err
}

func (b *appBackend) workstreamCloseUnusedAgent(id string, cause error) error {
closeAgent := b.workstreams.closeAgent
if closeAgent == nil {
closeAgent = func(id string) error { return b.MoveToClosed([]string{id}) }
}
b.workstreamChanged(change)
if change.ID != "" {
b.collectionChanged(w.ID, false)
if cleanup := closeAgent(id); cleanup != nil {
var wire *appwire.Error
if cause != nil && errors.As(appWorkstreamError(cause), &wire) {
copy := *wire
copy.Message += "; the unused workstream agent could not be closed"
return &copy
}
return appwire.Errorf(appwire.CodeEngine, "the unused workstream agent could not be closed: %s", appUnaskedReason("the conversation", cleanup))
}
return appwire.WorkstreamCreateResult{Workstream: appWorkstream(w, nil, b.workstreamSpend(w.AgentID))}, nil
return nil
}

func (b *appBackend) WorkstreamUpdate(p appwire.WorkstreamUpdateParams) (appwire.WorkstreamUpdateResult, error) {
Expand All @@ -370,6 +392,7 @@ func (b *appBackend) WorkstreamUpdate(p appwire.WorkstreamUpdateParams) (appwire
if err != nil {
return appwire.WorkstreamUpdateResult{}, c, err
}
b.workstreamRememberBrief(w)
if c.ID != "" && p.Patch.Name != nil {
b.collectionChanged(w.ID, false)
}
Expand All @@ -385,6 +408,7 @@ func (b *appBackend) WorkstreamMarkDone(p appwire.WorkstreamMarkDoneParams) (app
if err != nil {
return appwire.WorkstreamMarkDoneResult{}, c, err
}
b.workstreamRememberBrief(w)
facts := b.workstreamFacts(w.Jobs())
w, c, err = b.workstreamObserved(s, w, facts, c)
// The workstream is already durable even if archiving a chat fails. Publish
Expand Down Expand Up @@ -422,6 +446,7 @@ func (b *appBackend) WorkstreamReopen(p appwire.WorkstreamReopenParams) (appwire
if err != nil {
return appwire.WorkstreamReopenResult{}, c, err
}
b.workstreamRememberBrief(w)
facts := b.workstreamFacts(w.Jobs())
w, c, err = b.workstreamObserved(s, w, facts, c)
return appwire.WorkstreamReopenResult{Workstream: appWorkstream(w, facts, b.workstreamSpend(w.AgentID))}, c, err
Expand Down
Loading
Loading